feat: decouple views from projects (#2217)
This PR decouples views from projects. On the surface, everything stays the same - by default, there are the same views as right now in main - List, Gantt, Table, Kanban. With this feature, it is possible to modify these or create new ones. That means you can remove views you never need or create multiple ones if you need different configurations. Each view can have an optional filter to change what you see in the frontend on that view. For kanban, you can either set it to "manual" mode, where you can create buckets and move tasks around, or "filter" mode, where each bucket is the result of a filter (and you cannot move them around). All positions and buckets are now tied to the view, not the project. This means you can (finally!) have views for saved filters. Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2217
This commit is contained in:
commit
7230db1603
@ -55,19 +55,20 @@ This document describes the different errors Vikunja can return.
|
|||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
||||||
| ErrorCode | HTTP Status Code | Description |
|
| ErrorCode | HTTP Status Code | Description |
|
||||||
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------|------------------|------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| 3001 | 404 | The project does not exist. |
|
| 3001 | 404 | The project does not exist. |
|
||||||
| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
|
| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
|
||||||
| 3005 | 400 | The project title cannot be empty. |
|
| 3005 | 400 | The project title cannot be empty. |
|
||||||
| 3006 | 404 | The project share does not exist. |
|
| 3006 | 404 | The project share does not exist. |
|
||||||
| 3007 | 400 | A project with this identifier already exists. |
|
| 3007 | 400 | A project with this identifier already exists. |
|
||||||
| 3008 | 412 | The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project. |
|
| 3008 | 412 | The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project. |
|
||||||
| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". |
|
| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". |
|
||||||
| 3010 | 412 | This project cannot be a child of itself. |
|
| 3010 | 412 | This project cannot be a child of itself. |
|
||||||
| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. |
|
| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. |
|
||||||
| 3012 | 412 | This project cannot be deleted because a user has set it as their default project. |
|
| 3012 | 412 | This project cannot be deleted because a user has set it as their default project. |
|
||||||
| 3013 | 412 | This project cannot be archived because a user has set it as their default project. |
|
| 3013 | 412 | This project cannot be archived because a user has set it as their default project. |
|
||||||
|
| 3014 | 404 | This project view does not exist. |
|
||||||
|
|
||||||
## Task
|
## Task
|
||||||
|
|
||||||
@ -98,6 +99,7 @@ This document describes the different errors Vikunja can return.
|
|||||||
| 4023 | 409 | Tried to create a task relation which would create a cycle. |
|
| 4023 | 409 | Tried to create a task relation which would create a cycle. |
|
||||||
| 4024 | 400 | The provided filter expression is invalid. |
|
| 4024 | 400 | The provided filter expression is invalid. |
|
||||||
| 4025 | 400 | The reaction kind is invalid. |
|
| 4025 | 400 | The reaction kind is invalid. |
|
||||||
|
| 4026 | 400 | You must provide a project view ID when sorting by position. |
|
||||||
|
|
||||||
## Team
|
## Team
|
||||||
|
|
||||||
|
@ -1,15 +1,50 @@
|
|||||||
import {ProjectFactory} from '../../factories/project'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
import {ProjectViewFactory} from "../../factories/project_view";
|
||||||
|
|
||||||
|
export function createDefaultViews(projectId) {
|
||||||
|
ProjectViewFactory.truncate()
|
||||||
|
const list = ProjectViewFactory.create(1, {
|
||||||
|
id: 1,
|
||||||
|
project_id: projectId,
|
||||||
|
view_kind: 0,
|
||||||
|
}, false)
|
||||||
|
const gantt = ProjectViewFactory.create(1, {
|
||||||
|
id: 2,
|
||||||
|
project_id: projectId,
|
||||||
|
view_kind: 1,
|
||||||
|
}, false)
|
||||||
|
const table = ProjectViewFactory.create(1, {
|
||||||
|
id: 3,
|
||||||
|
project_id: projectId,
|
||||||
|
view_kind: 2,
|
||||||
|
}, false)
|
||||||
|
const kanban = ProjectViewFactory.create(1, {
|
||||||
|
id: 4,
|
||||||
|
project_id: projectId,
|
||||||
|
view_kind: 3,
|
||||||
|
bucket_configuration_mode: 1,
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
return [
|
||||||
|
list[0],
|
||||||
|
gantt[0],
|
||||||
|
table[0],
|
||||||
|
kanban[0],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export function createProjects() {
|
export function createProjects() {
|
||||||
const projects = ProjectFactory.create(1, {
|
const projects = ProjectFactory.create(1, {
|
||||||
title: 'First Project'
|
title: 'First Project'
|
||||||
})
|
})
|
||||||
TaskFactory.truncate()
|
TaskFactory.truncate()
|
||||||
|
projects.views = createDefaultViews(projects[0].id)
|
||||||
return projects
|
return projects
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareProjects(setProjects = (...args: any[]) => {}) {
|
export function prepareProjects(setProjects = (...args: any[]) => {
|
||||||
|
}) {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const projects = createProjects()
|
const projects = createProjects()
|
||||||
setProjects(projects)
|
setProjects(projects)
|
||||||
|
@ -2,6 +2,7 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|||||||
|
|
||||||
import {ProjectFactory} from '../../factories/project'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {prepareProjects} from './prepareProjects'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
import {ProjectViewFactory} from '../../factories/project_view'
|
||||||
|
|
||||||
describe('Project History', () => {
|
describe('Project History', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
@ -12,23 +13,28 @@ describe('Project History', () => {
|
|||||||
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
|
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
|
||||||
|
|
||||||
const projects = ProjectFactory.create(6)
|
const projects = ProjectFactory.create(6)
|
||||||
|
ProjectViewFactory.truncate()
|
||||||
|
projects.forEach(p => ProjectViewFactory.create(1, {
|
||||||
|
id: p.id,
|
||||||
|
project_id: p.id,
|
||||||
|
}, false))
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.wait('@loadProjectArray')
|
cy.wait('@loadProjectArray')
|
||||||
cy.get('body')
|
cy.get('body')
|
||||||
.should('not.contain', 'Last viewed')
|
.should('not.contain', 'Last viewed')
|
||||||
|
|
||||||
cy.visit(`/projects/${projects[0].id}`)
|
cy.visit(`/projects/${projects[0].id}/${projects[0].id}`)
|
||||||
cy.wait('@loadProject')
|
cy.wait('@loadProject')
|
||||||
cy.visit(`/projects/${projects[1].id}`)
|
cy.visit(`/projects/${projects[1].id}/${projects[1].id}`)
|
||||||
cy.wait('@loadProject')
|
cy.wait('@loadProject')
|
||||||
cy.visit(`/projects/${projects[2].id}`)
|
cy.visit(`/projects/${projects[2].id}/${projects[2].id}`)
|
||||||
cy.wait('@loadProject')
|
cy.wait('@loadProject')
|
||||||
cy.visit(`/projects/${projects[3].id}`)
|
cy.visit(`/projects/${projects[3].id}/${projects[3].id}`)
|
||||||
cy.wait('@loadProject')
|
cy.wait('@loadProject')
|
||||||
cy.visit(`/projects/${projects[4].id}`)
|
cy.visit(`/projects/${projects[4].id}/${projects[4].id}`)
|
||||||
cy.wait('@loadProject')
|
cy.wait('@loadProject')
|
||||||
cy.visit(`/projects/${projects[5].id}`)
|
cy.visit(`/projects/${projects[5].id}/${projects[5].id}`)
|
||||||
cy.wait('@loadProject')
|
cy.wait('@loadProject')
|
||||||
|
|
||||||
// cy.visit('/')
|
// cy.visit('/')
|
||||||
|
@ -11,7 +11,7 @@ describe('Project View Gantt', () => {
|
|||||||
|
|
||||||
it('Hides tasks with no dates', () => {
|
it('Hides tasks with no dates', () => {
|
||||||
const tasks = TaskFactory.create(1)
|
const tasks = TaskFactory.create(1)
|
||||||
cy.visit('/projects/1/gantt')
|
cy.visit('/projects/1/2')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
cy.get('.g-gantt-rows-container')
|
||||||
.should('not.contain', tasks[0].title)
|
.should('not.contain', tasks[0].title)
|
||||||
@ -25,7 +25,7 @@ describe('Project View Gantt', () => {
|
|||||||
nextMonth.setDate(1)
|
nextMonth.setDate(1)
|
||||||
nextMonth.setMonth(9)
|
nextMonth.setMonth(9)
|
||||||
|
|
||||||
cy.visit('/projects/1/gantt')
|
cy.visit('/projects/1/2')
|
||||||
|
|
||||||
cy.get('.g-timeunits-container')
|
cy.get('.g-timeunits-container')
|
||||||
.should('contain', format(now, 'MMMM'))
|
.should('contain', format(now, 'MMMM'))
|
||||||
@ -38,7 +38,7 @@ describe('Project View Gantt', () => {
|
|||||||
start_date: now.toISOString(),
|
start_date: now.toISOString(),
|
||||||
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
||||||
})
|
})
|
||||||
cy.visit('/projects/1/gantt')
|
cy.visit('/projects/1/2')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container')
|
cy.get('.g-gantt-rows-container')
|
||||||
.should('not.be.empty')
|
.should('not.be.empty')
|
||||||
@ -50,7 +50,7 @@ describe('Project View Gantt', () => {
|
|||||||
start_date: null,
|
start_date: null,
|
||||||
end_date: null,
|
end_date: null,
|
||||||
})
|
})
|
||||||
cy.visit('/projects/1/gantt')
|
cy.visit('/projects/1/2')
|
||||||
|
|
||||||
cy.get('.gantt-options .fancycheckbox')
|
cy.get('.gantt-options .fancycheckbox')
|
||||||
.contains('Show tasks which don\'t have dates set')
|
.contains('Show tasks which don\'t have dates set')
|
||||||
@ -69,7 +69,7 @@ describe('Project View Gantt', () => {
|
|||||||
start_date: now.toISOString(),
|
start_date: now.toISOString(),
|
||||||
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
|
||||||
})
|
})
|
||||||
cy.visit('/projects/1/gantt')
|
cy.visit('/projects/1/2')
|
||||||
|
|
||||||
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
||||||
.first()
|
.first()
|
||||||
@ -83,7 +83,7 @@ describe('Project View Gantt', () => {
|
|||||||
const now = Date.UTC(2022, 10, 9)
|
const now = Date.UTC(2022, 10, 9)
|
||||||
cy.clock(now, ['Date'])
|
cy.clock(now, ['Date'])
|
||||||
|
|
||||||
cy.visit('/projects/1/gantt')
|
cy.visit('/projects/1/2')
|
||||||
|
|
||||||
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
|
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
|
||||||
.click()
|
.click()
|
||||||
@ -99,7 +99,7 @@ describe('Project View Gantt', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should change the date range based on date query parameters', () => {
|
it('Should change the date range based on date query parameters', () => {
|
||||||
cy.visit('/projects/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
|
cy.visit('/projects/1/2?dateFrom=2022-09-25&dateTo=2022-11-05')
|
||||||
|
|
||||||
cy.get('.g-timeunits-container')
|
cy.get('.g-timeunits-container')
|
||||||
.should('contain', 'September 2022')
|
.should('contain', 'September 2022')
|
||||||
@ -115,7 +115,7 @@ describe('Project View Gantt', () => {
|
|||||||
start_date: formatISO(now),
|
start_date: formatISO(now),
|
||||||
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
end_date: formatISO(now.setDate(now.getDate() + 4)),
|
||||||
})
|
})
|
||||||
cy.visit('/projects/1/gantt')
|
cy.visit('/projects/1/2')
|
||||||
|
|
||||||
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
|
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
|
||||||
.dblclick()
|
.dblclick()
|
||||||
|
@ -4,18 +4,49 @@ import {BucketFactory} from '../../factories/bucket'
|
|||||||
import {ProjectFactory} from '../../factories/project'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {prepareProjects} from './prepareProjects'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
import {ProjectViewFactory} from "../../factories/project_view";
|
||||||
|
import {TaskBucketFactory} from "../../factories/task_buckets";
|
||||||
|
|
||||||
function createSingleTaskInBucket(count = 1, attrs = {}) {
|
function createSingleTaskInBucket(count = 1, attrs = {}) {
|
||||||
const projects = ProjectFactory.create(1)
|
const projects = ProjectFactory.create(1)
|
||||||
const buckets = BucketFactory.create(2, {
|
const views = ProjectViewFactory.create(1, {
|
||||||
|
id: 1,
|
||||||
project_id: projects[0].id,
|
project_id: projects[0].id,
|
||||||
|
view_kind: 3,
|
||||||
|
bucket_configuration_mode: 1,
|
||||||
|
})
|
||||||
|
const buckets = BucketFactory.create(2, {
|
||||||
|
project_view_id: views[0].id,
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(count, {
|
const tasks = TaskFactory.create(count, {
|
||||||
project_id: projects[0].id,
|
project_id: projects[0].id,
|
||||||
bucket_id: buckets[0].id,
|
bucket_id: buckets[0].id,
|
||||||
...attrs,
|
...attrs,
|
||||||
})
|
})
|
||||||
return tasks[0]
|
TaskBucketFactory.create(1, {
|
||||||
|
task_id: tasks[0].id,
|
||||||
|
bucket_id: buckets[0].id,
|
||||||
|
project_view_id: views[0].id,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
task: tasks[0],
|
||||||
|
view: views[0],
|
||||||
|
project: projects[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTaskWithBuckets(buckets, count = 1) {
|
||||||
|
const data = TaskFactory.create(10, {
|
||||||
|
project_id: 1,
|
||||||
|
})
|
||||||
|
TaskBucketFactory.truncate()
|
||||||
|
data.forEach(t => TaskBucketFactory.create(count, {
|
||||||
|
task_id: t.id,
|
||||||
|
bucket_id: buckets[0].id,
|
||||||
|
project_view_id: buckets[0].project_view_id,
|
||||||
|
}, false))
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Project View Kanban', () => {
|
describe('Project View Kanban', () => {
|
||||||
@ -24,15 +55,14 @@ describe('Project View Kanban', () => {
|
|||||||
|
|
||||||
let buckets
|
let buckets
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
buckets = BucketFactory.create(2)
|
buckets = BucketFactory.create(2, {
|
||||||
|
project_view_id: 4,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Shows all buckets with their tasks', () => {
|
it('Shows all buckets with their tasks', () => {
|
||||||
const data = TaskFactory.create(10, {
|
const data = createTaskWithBuckets(buckets, 10)
|
||||||
project_id: 1,
|
cy.visit('/projects/1/4')
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .title')
|
cy.get('.kanban .bucket .title')
|
||||||
.contains(buckets[0].title)
|
.contains(buckets[0].title)
|
||||||
@ -46,11 +76,8 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Can add a new task to a bucket', () => {
|
it('Can add a new task to a bucket', () => {
|
||||||
TaskFactory.create(2, {
|
createTaskWithBuckets(buckets, 2)
|
||||||
project_id: 1,
|
cy.visit('/projects/1/4')
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket')
|
cy.get('.kanban .bucket')
|
||||||
.contains(buckets[0].title)
|
.contains(buckets[0].title)
|
||||||
@ -68,7 +95,7 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Can create a new bucket', () => {
|
it('Can create a new bucket', () => {
|
||||||
cy.visit('/projects/1/kanban')
|
cy.visit('/projects/1/4')
|
||||||
|
|
||||||
cy.get('.kanban .bucket.new-bucket .button')
|
cy.get('.kanban .bucket.new-bucket .button')
|
||||||
.click()
|
.click()
|
||||||
@ -82,7 +109,7 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Can set a bucket limit', () => {
|
it('Can set a bucket limit', () => {
|
||||||
cy.visit('/projects/1/kanban')
|
cy.visit('/projects/1/4')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||||
.first()
|
.first()
|
||||||
@ -103,7 +130,7 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Can rename a bucket', () => {
|
it('Can rename a bucket', () => {
|
||||||
cy.visit('/projects/1/kanban')
|
cy.visit('/projects/1/4')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .title')
|
cy.get('.kanban .bucket .bucket-header .title')
|
||||||
.first()
|
.first()
|
||||||
@ -114,7 +141,7 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Can delete a bucket', () => {
|
it('Can delete a bucket', () => {
|
||||||
cy.visit('/projects/1/kanban')
|
cy.visit('/projects/1/4')
|
||||||
|
|
||||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||||
.first()
|
.first()
|
||||||
@ -137,11 +164,8 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Can drag tasks around', () => {
|
it('Can drag tasks around', () => {
|
||||||
const tasks = TaskFactory.create(2, {
|
const tasks = createTaskWithBuckets(buckets, 2)
|
||||||
project_id: 1,
|
cy.visit('/projects/1/4')
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
@ -155,12 +179,8 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should navigate to the task when the task card is clicked', () => {
|
it('Should navigate to the task when the task card is clicked', () => {
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = createTaskWithBuckets(buckets, 5)
|
||||||
id: '{increment}',
|
cy.visit('/projects/1/4')
|
||||||
project_id: 1,
|
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
@ -168,28 +188,33 @@ describe('Project View Kanban', () => {
|
|||||||
.click()
|
.click()
|
||||||
|
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
|
.should('contain', `/tasks/${tasks[0].id}`, {timeout: 1000})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should remove a task from the kanban board when moving it to another project', () => {
|
it('Should remove a task from the kanban board when moving it to another project', () => {
|
||||||
const projects = ProjectFactory.create(2)
|
const projects = ProjectFactory.create(2)
|
||||||
BucketFactory.create(2, {
|
const views = ProjectViewFactory.create(2, {
|
||||||
project_id: '{increment}',
|
project_id: '{increment}',
|
||||||
|
view_kind: 3,
|
||||||
|
bucket_configuration_mode: 1,
|
||||||
})
|
})
|
||||||
|
BucketFactory.create(2)
|
||||||
const tasks = TaskFactory.create(5, {
|
const tasks = TaskFactory.create(5, {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
project_id: 1,
|
project_id: 1,
|
||||||
bucket_id: 1,
|
})
|
||||||
|
TaskBucketFactory.create(5, {
|
||||||
|
project_view_id: 1,
|
||||||
})
|
})
|
||||||
const task = tasks[0]
|
const task = tasks[0]
|
||||||
cy.visit('/projects/1/kanban')
|
cy.visit('/projects/1/'+views[0].id)
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(task.title)
|
.contains(task.title)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
cy.get('.task-view .action-buttons .button', { timeout: 3000 })
|
cy.get('.task-view .action-buttons .button', {timeout: 3000})
|
||||||
.contains('Move')
|
.contains('Move')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
||||||
@ -201,7 +226,7 @@ describe('Project View Kanban', () => {
|
|||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
cy.get('.global-notification', { timeout: 1000 })
|
cy.get('.global-notification', {timeout: 1000})
|
||||||
.should('contain', 'Success')
|
.should('contain', 'Success')
|
||||||
cy.go('back')
|
cy.go('back')
|
||||||
cy.get('.kanban .bucket')
|
cy.get('.kanban .bucket')
|
||||||
@ -209,19 +234,15 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Shows a button to filter the kanban board', () => {
|
it('Shows a button to filter the kanban board', () => {
|
||||||
const data = TaskFactory.create(10, {
|
cy.visit('/projects/1/4')
|
||||||
project_id: 1,
|
|
||||||
bucket_id: 1,
|
|
||||||
})
|
|
||||||
cy.visit('/projects/1/kanban')
|
|
||||||
|
|
||||||
cy.get('.project-kanban .filter-container .base-button')
|
cy.get('.project-kanban .filter-container .base-button')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should remove a task from the board when deleting it', () => {
|
it('Should remove a task from the board when deleting it', () => {
|
||||||
const task = createSingleTaskInBucket(5)
|
const {task, view} = createSingleTaskInBucket(5)
|
||||||
cy.visit('/projects/1/kanban')
|
cy.visit(`/projects/1/${view.id}`)
|
||||||
|
|
||||||
cy.get('.kanban .bucket .tasks .task')
|
cy.get('.kanban .bucket .tasks .task')
|
||||||
.contains(task.title)
|
.contains(task.title)
|
||||||
@ -245,12 +266,12 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should show a task description icon if the task has a description', () => {
|
it('Should show a task description icon if the task has a description', () => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
|
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
|
||||||
const task = createSingleTaskInBucket(1, {
|
const {task, view} = createSingleTaskInBucket(1, {
|
||||||
description: 'Lorem Ipsum',
|
description: 'Lorem Ipsum',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit(`/projects/${task.project_id}/kanban`)
|
cy.visit(`/projects/${task.project_id}/${view.id}`)
|
||||||
cy.wait('@loadTasks')
|
cy.wait('@loadTasks')
|
||||||
|
|
||||||
cy.get('.bucket .tasks .task .footer .icon svg')
|
cy.get('.bucket .tasks .task .footer .icon svg')
|
||||||
@ -258,12 +279,12 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should not show a task description icon if the task has an empty description', () => {
|
it('Should not show a task description icon if the task has an empty description', () => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
|
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
|
||||||
const task = createSingleTaskInBucket(1, {
|
const {task, view} = createSingleTaskInBucket(1, {
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit(`/projects/${task.project_id}/kanban`)
|
cy.visit(`/projects/${task.project_id}/${view.id}`)
|
||||||
cy.wait('@loadTasks')
|
cy.wait('@loadTasks')
|
||||||
|
|
||||||
cy.get('.bucket .tasks .task .footer .icon svg')
|
cy.get('.bucket .tasks .task .footer .icon svg')
|
||||||
@ -271,12 +292,12 @@ describe('Project View Kanban', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
|
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
|
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
|
||||||
const task = createSingleTaskInBucket(1, {
|
const {task, view} = createSingleTaskInBucket(1, {
|
||||||
description: '<p></p>',
|
description: '<p></p>',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit(`/projects/${task.project_id}/kanban`)
|
cy.visit(`/projects/${task.project_id}/${view.id}`)
|
||||||
cy.wait('@loadTasks')
|
cy.wait('@loadTasks')
|
||||||
|
|
||||||
cy.get('.bucket .tasks .task .footer .icon svg')
|
cy.get('.bucket .tasks .task .footer .icon svg')
|
||||||
|
@ -5,15 +5,16 @@ import {TaskFactory} from '../../factories/task'
|
|||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
import {ProjectFactory} from '../../factories/project'
|
import {ProjectFactory} from '../../factories/project'
|
||||||
import {prepareProjects} from './prepareProjects'
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
|
|
||||||
describe('Project View Project', () => {
|
describe('Project View List', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
prepareProjects()
|
prepareProjects()
|
||||||
|
|
||||||
it('Should be an empty project', () => {
|
it('Should be an empty project', () => {
|
||||||
cy.visit('/projects/1')
|
cy.visit('/projects/1')
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', '/projects/1/list')
|
.should('contain', '/projects/1/1')
|
||||||
cy.get('.project-title')
|
cy.get('.project-title')
|
||||||
.should('contain', 'First Project')
|
.should('contain', 'First Project')
|
||||||
cy.get('.project-title-dropdown')
|
cy.get('.project-title-dropdown')
|
||||||
@ -24,6 +25,10 @@ describe('Project View Project', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should create a new task', () => {
|
it('Should create a new task', () => {
|
||||||
|
BucketFactory.create(2, {
|
||||||
|
project_view_id: 4,
|
||||||
|
})
|
||||||
|
|
||||||
const newTaskTitle = 'New task'
|
const newTaskTitle = 'New task'
|
||||||
|
|
||||||
cy.visit('/projects/1')
|
cy.visit('/projects/1')
|
||||||
@ -38,7 +43,7 @@ describe('Project View Project', () => {
|
|||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
project_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
|
|
||||||
cy.get('.tasks .task .tasktext')
|
cy.get('.tasks .task .tasktext')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
@ -88,10 +93,10 @@ describe('Project View Project', () => {
|
|||||||
title: i => `task${i}`,
|
title: i => `task${i}`,
|
||||||
project_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
|
|
||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
.should('contain', tasks[1].title)
|
.should('contain', tasks[20].title)
|
||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
.should('not.contain', tasks[99].title)
|
.should('not.contain', tasks[99].title)
|
||||||
|
|
||||||
@ -104,6 +109,6 @@ describe('Project View Project', () => {
|
|||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
.should('contain', tasks[99].title)
|
.should('contain', tasks[99].title)
|
||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
.should('not.contain', tasks[1].title)
|
.should('not.contain', tasks[20].title)
|
||||||
})
|
})
|
||||||
})
|
})
|
@ -1,13 +1,15 @@
|
|||||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||||
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
import {prepareProjects} from './prepareProjects'
|
||||||
|
|
||||||
describe('Project View Table', () => {
|
describe('Project View Table', () => {
|
||||||
createFakeUserAndLogin()
|
createFakeUserAndLogin()
|
||||||
|
prepareProjects()
|
||||||
|
|
||||||
it('Should show a table with tasks', () => {
|
it('Should show a table with tasks', () => {
|
||||||
const tasks = TaskFactory.create(1)
|
const tasks = TaskFactory.create(1)
|
||||||
cy.visit('/projects/1/table')
|
cy.visit('/projects/1/3')
|
||||||
|
|
||||||
cy.get('.project-table table.table')
|
cy.get('.project-table table.table')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
@ -17,7 +19,7 @@ describe('Project View Table', () => {
|
|||||||
|
|
||||||
it('Should have working column switches', () => {
|
it('Should have working column switches', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
cy.visit('/projects/1/table')
|
cy.visit('/projects/1/3')
|
||||||
|
|
||||||
cy.get('.project-table .filter-container .items .button')
|
cy.get('.project-table .filter-container .items .button')
|
||||||
.contains('Columns')
|
.contains('Columns')
|
||||||
@ -42,7 +44,7 @@ describe('Project View Table', () => {
|
|||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
project_id: 1,
|
project_id: 1,
|
||||||
})
|
})
|
||||||
cy.visit('/projects/1/table')
|
cy.visit('/projects/1/3')
|
||||||
|
|
||||||
cy.get('.project-table table.table')
|
cy.get('.project-table table.table')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
@ -33,14 +33,14 @@ describe('Projects', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should redirect to a specific project view after visited', () => {
|
it('Should redirect to a specific project view after visited', () => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/*/buckets*').as('loadBuckets')
|
cy.intercept(Cypress.env('API_URL') + '/projects/*/views/*/tasks**').as('loadBuckets')
|
||||||
cy.visit('/projects/1/kanban')
|
cy.visit('/projects/1/4')
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', '/projects/1/kanban')
|
.should('contain', '/projects/1/4')
|
||||||
cy.wait('@loadBuckets')
|
cy.wait('@loadBuckets')
|
||||||
cy.visit('/projects/1')
|
cy.visit('/projects/1')
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', '/projects/1/kanban')
|
.should('contain', '/projects/1/4')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should rename the project in all places', () => {
|
it('Should rename the project in all places', () => {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import {LinkShareFactory} from '../../factories/link_sharing'
|
import {LinkShareFactory} from '../../factories/link_sharing'
|
||||||
import {ProjectFactory} from '../../factories/project'
|
|
||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
|
import {createProjects} from '../project/prepareProjects'
|
||||||
|
|
||||||
function prepareLinkShare() {
|
function prepareLinkShare() {
|
||||||
const projects = ProjectFactory.create(1)
|
const projects = createProjects()
|
||||||
const tasks = TaskFactory.create(10, {
|
const tasks = TaskFactory.create(10, {
|
||||||
project_id: projects[0].id
|
project_id: projects[0].id
|
||||||
})
|
})
|
||||||
@ -32,13 +32,13 @@ describe('Link shares', () => {
|
|||||||
cy.get('.tasks')
|
cy.get('.tasks')
|
||||||
.should('contain', tasks[0].title)
|
.should('contain', tasks[0].title)
|
||||||
|
|
||||||
cy.url().should('contain', `/projects/${project.id}/list#share-auth-token=${share.hash}`)
|
cy.url().should('contain', `/projects/${project.id}/1#share-auth-token=${share.hash}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should work when directly viewing a project with share hash present', () => {
|
it('Should work when directly viewing a project with share hash present', () => {
|
||||||
const {share, project, tasks} = prepareLinkShare()
|
const {share, project, tasks} = prepareLinkShare()
|
||||||
|
|
||||||
cy.visit(`/projects/${project.id}/list#share-auth-token=${share.hash}`)
|
cy.visit(`/projects/${project.id}/1#share-auth-token=${share.hash}`)
|
||||||
|
|
||||||
cy.get('h1.title')
|
cy.get('h1.title')
|
||||||
.should('contain', project.title)
|
.should('contain', project.title)
|
||||||
|
@ -5,11 +5,13 @@ import {seed} from '../../support/seed'
|
|||||||
import {TaskFactory} from '../../factories/task'
|
import {TaskFactory} from '../../factories/task'
|
||||||
import {BucketFactory} from '../../factories/bucket'
|
import {BucketFactory} from '../../factories/bucket'
|
||||||
import {updateUserSettings} from '../../support/updateUserSettings'
|
import {updateUserSettings} from '../../support/updateUserSettings'
|
||||||
|
import {createDefaultViews} from "../project/prepareProjects";
|
||||||
|
|
||||||
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||||
const project = ProjectFactory.create()[0]
|
const project = ProjectFactory.create()[0]
|
||||||
|
const views = createDefaultViews(project.id)
|
||||||
BucketFactory.create(1, {
|
BucketFactory.create(1, {
|
||||||
project_id: project.id,
|
project_view_id: views[3].id,
|
||||||
})
|
})
|
||||||
const tasks = []
|
const tasks = []
|
||||||
let dueDate = startDueDate
|
let dueDate = startDueDate
|
||||||
@ -60,7 +62,7 @@ describe('Home Page Task Overview', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should show a new task with a very soon due date at the top', () => {
|
it('Should show a new task with a very soon due date at the top', () => {
|
||||||
const {tasks} = seedTasks()
|
const {tasks} = seedTasks(49)
|
||||||
const newTaskTitle = 'New Task'
|
const newTaskTitle = 'New Task'
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
@ -71,9 +73,8 @@ describe('Home Page Task Overview', () => {
|
|||||||
due_date: new Date().toISOString(),
|
due_date: new Date().toISOString(),
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
cy.visit(`/projects/${tasks[0].project_id}/list`)
|
cy.visit(`/projects/${tasks[0].project_id}/1`)
|
||||||
cy.get('.tasks .task')
|
cy.get('.tasks .task')
|
||||||
.first()
|
|
||||||
.should('contain.text', newTaskTitle)
|
.should('contain.text', newTaskTitle)
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.get('[data-cy="showTasks"] .card .task')
|
cy.get('[data-cy="showTasks"] .card .task')
|
||||||
@ -88,7 +89,7 @@ describe('Home Page Task Overview', () => {
|
|||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
|
||||||
cy.visit(`/projects/${tasks[0].project_id}/list`)
|
cy.visit(`/projects/${tasks[0].project_id}/1`)
|
||||||
cy.get('.task-add textarea')
|
cy.get('.task-add textarea')
|
||||||
.type(newTaskTitle+'{enter}')
|
.type(newTaskTitle+'{enter}')
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
|
@ -12,6 +12,7 @@ import {BucketFactory} from '../../factories/bucket'
|
|||||||
|
|
||||||
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
||||||
import {TaskReminderFactory} from '../../factories/task_reminders'
|
import {TaskReminderFactory} from '../../factories/task_reminders'
|
||||||
|
import {createDefaultViews} from "../project/prepareProjects";
|
||||||
|
|
||||||
function addLabelToTaskAndVerify(labelTitle: string) {
|
function addLabelToTaskAndVerify(labelTitle: string) {
|
||||||
cy.get('.task-view .action-buttons .button')
|
cy.get('.task-view .action-buttons .button')
|
||||||
@ -53,15 +54,16 @@ describe('Task', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// UserFactory.create(1)
|
// UserFactory.create(1)
|
||||||
projects = ProjectFactory.create(1)
|
projects = ProjectFactory.create(1)
|
||||||
|
const views = createDefaultViews(projects[0].id)
|
||||||
buckets = BucketFactory.create(1, {
|
buckets = BucketFactory.create(1, {
|
||||||
project_id: projects[0].id,
|
project_view_id: views[3].id,
|
||||||
})
|
})
|
||||||
TaskFactory.truncate()
|
TaskFactory.truncate()
|
||||||
UserProjectFactory.truncate()
|
UserProjectFactory.truncate()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should be created new', () => {
|
it('Should be created new', () => {
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
cy.get('.input[placeholder="Add a new task…"')
|
cy.get('.input[placeholder="Add a new task…"')
|
||||||
.type('New Task')
|
.type('New Task')
|
||||||
cy.get('.button')
|
cy.get('.button')
|
||||||
@ -75,7 +77,7 @@ describe('Task', () => {
|
|||||||
it('Inserts new tasks at the top of the project', () => {
|
it('Inserts new tasks at the top of the project', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
cy.get('.project-is-empty-notice')
|
cy.get('.project-is-empty-notice')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.input[placeholder="Add a new task…"')
|
cy.get('.input[placeholder="Add a new task…"')
|
||||||
@ -93,7 +95,7 @@ describe('Task', () => {
|
|||||||
it('Marks a task as done', () => {
|
it('Marks a task as done', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
cy.get('.tasks .task .fancycheckbox')
|
cy.get('.tasks .task .fancycheckbox')
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
@ -104,7 +106,7 @@ describe('Task', () => {
|
|||||||
it('Can add a task to favorites', () => {
|
it('Can add a task to favorites', () => {
|
||||||
TaskFactory.create(1)
|
TaskFactory.create(1)
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
cy.get('.tasks .task .favorite')
|
cy.get('.tasks .task .favorite')
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
@ -113,12 +115,12 @@ describe('Task', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should show a task description icon if the task has a description', () => {
|
it('Should show a task description icon if the task has a description', () => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
|
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
|
||||||
TaskFactory.create(1, {
|
TaskFactory.create(1, {
|
||||||
description: 'Lorem Ipsum',
|
description: 'Lorem Ipsum',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
cy.wait('@loadTasks')
|
cy.wait('@loadTasks')
|
||||||
|
|
||||||
cy.get('.tasks .task .project-task-icon')
|
cy.get('.tasks .task .project-task-icon')
|
||||||
@ -126,12 +128,12 @@ describe('Task', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should not show a task description icon if the task has an empty description', () => {
|
it('Should not show a task description icon if the task has an empty description', () => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
|
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
|
||||||
TaskFactory.create(1, {
|
TaskFactory.create(1, {
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
cy.wait('@loadTasks')
|
cy.wait('@loadTasks')
|
||||||
|
|
||||||
cy.get('.tasks .task .project-task-icon')
|
cy.get('.tasks .task .project-task-icon')
|
||||||
@ -139,12 +141,12 @@ describe('Task', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
|
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
|
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
|
||||||
TaskFactory.create(1, {
|
TaskFactory.create(1, {
|
||||||
description: '<p></p>',
|
description: '<p></p>',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.visit('/projects/1/list')
|
cy.visit('/projects/1/1')
|
||||||
cy.wait('@loadTasks')
|
cy.wait('@loadTasks')
|
||||||
|
|
||||||
cy.get('.tasks .task .project-task-icon')
|
cy.get('.tasks .task .project-task-icon')
|
||||||
@ -314,8 +316,9 @@ describe('Task', () => {
|
|||||||
|
|
||||||
it('Can move a task to another project', () => {
|
it('Can move a task to another project', () => {
|
||||||
const projects = ProjectFactory.create(2)
|
const projects = ProjectFactory.create(2)
|
||||||
|
const views = createDefaultViews(projects[0].id)
|
||||||
BucketFactory.create(2, {
|
BucketFactory.create(2, {
|
||||||
project_id: '{increment}',
|
project_view_id: views[3].id,
|
||||||
})
|
})
|
||||||
const tasks = TaskFactory.create(1, {
|
const tasks = TaskFactory.create(1, {
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -469,7 +472,7 @@ describe('Task', () => {
|
|||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.truncate()
|
LabelTaskFactory.truncate()
|
||||||
|
|
||||||
cy.visit(`/projects/${projects[0].id}/kanban`)
|
cy.visit(`/projects/${projects[0].id}/4`)
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
cy.get('.bucket .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
@ -836,7 +839,7 @@ describe('Task', () => {
|
|||||||
const labels = LabelFactory.create(1)
|
const labels = LabelFactory.create(1)
|
||||||
LabelTaskFactory.truncate()
|
LabelTaskFactory.truncate()
|
||||||
|
|
||||||
cy.visit(`/projects/${projects[0].id}/kanban`)
|
cy.visit(`/projects/${projects[0].id}/4`)
|
||||||
|
|
||||||
cy.get('.bucket .task')
|
cy.get('.bucket .task')
|
||||||
.contains(tasks[0].title)
|
.contains(tasks[0].title)
|
||||||
|
@ -10,7 +10,7 @@ export class BucketFactory extends Factory {
|
|||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
title: faker.lorem.words(3),
|
title: faker.lorem.words(3),
|
||||||
project_id: 1,
|
project_view_id: '{increment}',
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
created: now.toISOString(),
|
created: now.toISOString(),
|
||||||
updated: now.toISOString(),
|
updated: now.toISOString(),
|
||||||
|
19
frontend/cypress/factories/project_view.ts
Normal file
19
frontend/cypress/factories/project_view.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {Factory} from '../support/factory'
|
||||||
|
import {faker} from '@faker-js/faker'
|
||||||
|
|
||||||
|
export class ProjectViewFactory extends Factory {
|
||||||
|
static table = 'project_views'
|
||||||
|
|
||||||
|
static factory() {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: '{increment}',
|
||||||
|
title: faker.lorem.words(3),
|
||||||
|
project_id: '{increment}',
|
||||||
|
view_kind: 0,
|
||||||
|
created: now.toISOString(),
|
||||||
|
updated: now.toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,6 @@ export class TaskFactory extends Factory {
|
|||||||
project_id: 1,
|
project_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
index: '{increment}',
|
index: '{increment}',
|
||||||
position: '{increment}',
|
|
||||||
created: now.toISOString(),
|
created: now.toISOString(),
|
||||||
updated: now.toISOString()
|
updated: now.toISOString()
|
||||||
}
|
}
|
||||||
|
13
frontend/cypress/factories/task_buckets.ts
Normal file
13
frontend/cypress/factories/task_buckets.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {Factory} from '../support/factory'
|
||||||
|
|
||||||
|
export class TaskBucketFactory extends Factory {
|
||||||
|
static table = 'task_buckets'
|
||||||
|
|
||||||
|
static factory() {
|
||||||
|
return {
|
||||||
|
task_id: '{increment}',
|
||||||
|
bucket_id: '{increment}',
|
||||||
|
project_view_id: '{increment}',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@
|
|||||||
v-slot="{ Component }"
|
v-slot="{ Component }"
|
||||||
:route="routeWithModal"
|
:route="routeWithModal"
|
||||||
>
|
>
|
||||||
<keep-alive :include="['project.list', 'project.gantt', 'project.table', 'project.kanban']">
|
<keep-alive :include="['project.view']">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
@ -33,11 +33,15 @@ import {useBaseStore} from '@/stores/base'
|
|||||||
|
|
||||||
import Logo from '@/components/home/Logo.vue'
|
import Logo from '@/components/home/Logo.vue'
|
||||||
import PoweredByLink from './PoweredByLink.vue'
|
import PoweredByLink from './PoweredByLink.vue'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const currentProject = computed(() => baseStore.currentProject)
|
const currentProject = computed(() => baseStore.currentProject)
|
||||||
const background = computed(() => baseStore.background)
|
const background = computed(() => baseStore.background)
|
||||||
const logoVisible = computed(() => baseStore.logoVisible)
|
const logoVisible = computed(() => baseStore.logoVisible)
|
||||||
|
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
projectStore.loadAllProjects()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -62,7 +62,7 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'project.kanban.title',
|
title: 'project.kanban.title',
|
||||||
available: (route) => route.name === 'project.kanban',
|
available: (route) => route.name === 'project.view',
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
title: 'keyboardShortcuts.task.done',
|
title: 'keyboardShortcuts.task.done',
|
||||||
|
@ -10,40 +10,13 @@
|
|||||||
<div class="switch-view-container d-print-none">
|
<div class="switch-view-container d-print-none">
|
||||||
<div class="switch-view">
|
<div class="switch-view">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-shortcut="'g l'"
|
v-for="v in views"
|
||||||
:title="$t('keyboardShortcuts.project.switchToListView')"
|
:key="v.id"
|
||||||
class="switch-view-button"
|
class="switch-view-button"
|
||||||
:class="{'is-active': viewName === 'project'}"
|
:class="{'is-active': v.id === viewId}"
|
||||||
:to="{ name: 'project.list', params: { projectId } }"
|
:to="{ name: 'project.view', params: { projectId, viewId: v.id } }"
|
||||||
>
|
>
|
||||||
{{ $t('project.list.title') }}
|
{{ getViewTitle(v) }}
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-shortcut="'g g'"
|
|
||||||
:title="$t('keyboardShortcuts.project.switchToGanttView')"
|
|
||||||
class="switch-view-button"
|
|
||||||
:class="{'is-active': viewName === 'gantt'}"
|
|
||||||
:to="{ name: 'project.gantt', params: { projectId } }"
|
|
||||||
>
|
|
||||||
{{ $t('project.gantt.title') }}
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-shortcut="'g t'"
|
|
||||||
:title="$t('keyboardShortcuts.project.switchToTableView')"
|
|
||||||
class="switch-view-button"
|
|
||||||
:class="{'is-active': viewName === 'table'}"
|
|
||||||
:to="{ name: 'project.table', params: { projectId } }"
|
|
||||||
>
|
|
||||||
{{ $t('project.table.title') }}
|
|
||||||
</BaseButton>
|
|
||||||
<BaseButton
|
|
||||||
v-shortcut="'g k'"
|
|
||||||
:title="$t('keyboardShortcuts.project.switchToKanbanView')"
|
|
||||||
class="switch-view-button"
|
|
||||||
:class="{'is-active': viewName === 'kanban'}"
|
|
||||||
:to="{ name: 'project.kanban', params: { projectId } }"
|
|
||||||
>
|
|
||||||
{{ $t('project.kanban.title') }}
|
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
@ -63,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, watch} from 'vue'
|
import {computed, ref, watch} from 'vue'
|
||||||
import {useRoute} from 'vue-router'
|
import {useRoute} from 'vue-router'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
@ -79,26 +52,27 @@ import {useTitle} from '@/composables/useTitle'
|
|||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps({
|
const {
|
||||||
projectId: {
|
projectId,
|
||||||
type: Number,
|
viewId,
|
||||||
required: true,
|
} = defineProps<{
|
||||||
},
|
projectId: IProject['id'],
|
||||||
viewName: {
|
viewId: IProjectView['id'],
|
||||||
type: String,
|
}>()
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const {t} = useI18n()
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
const projectService = ref(new ProjectService())
|
const projectService = ref(new ProjectService())
|
||||||
const loadedProjectId = ref(0)
|
const loadedProjectId = ref(0)
|
||||||
|
|
||||||
const currentProject = computed(() => {
|
const currentProject = computed<IProject>(() => {
|
||||||
return typeof baseStore.currentProject === 'undefined' ? {
|
return typeof baseStore.currentProject === 'undefined' ? {
|
||||||
id: 0,
|
id: 0,
|
||||||
title: '',
|
title: '',
|
||||||
@ -108,13 +82,15 @@ const currentProject = computed(() => {
|
|||||||
})
|
})
|
||||||
useTitle(() => currentProject.value?.id ? getProjectTitle(currentProject.value) : '')
|
useTitle(() => currentProject.value?.id ? getProjectTitle(currentProject.value) : '')
|
||||||
|
|
||||||
|
const views = computed(() => currentProject.value?.views)
|
||||||
|
|
||||||
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
|
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
|
||||||
// This resulted in loading and setting the project multiple times, even when navigating away from it.
|
// This resulted in loading and setting the project multiple times, even when navigating away from it.
|
||||||
// This caused wired bugs where the project background would be set on the home page but only right after setting a new
|
// This caused wired bugs where the project background would be set on the home page but only right after setting a new
|
||||||
// project background and then navigating to home. It also highlighted the project in the menu and didn't allow changing any
|
// project background and then navigating to home. It also highlighted the project in the menu and didn't allow changing any
|
||||||
// of it, most likely due to the rights not being properly populated.
|
// of it, most likely due to the rights not being properly populated.
|
||||||
watch(
|
watch(
|
||||||
() => props.projectId,
|
() => projectId,
|
||||||
// loadProject
|
// loadProject
|
||||||
async (projectIdToLoad: number) => {
|
async (projectIdToLoad: number) => {
|
||||||
const projectData = {id: projectIdToLoad}
|
const projectData = {id: projectIdToLoad}
|
||||||
@ -130,11 +106,11 @@ watch(
|
|||||||
)
|
)
|
||||||
&& typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null
|
&& typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null
|
||||||
) {
|
) {
|
||||||
loadedProjectId.value = props.projectId
|
loadedProjectId.value = projectId
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`Loading project, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedProjectId = ${loadedProjectId.value}, currentProject = `, currentProject.value)
|
console.debug('Loading project, $route.params =', route.params, `, loadedProjectId = ${loadedProjectId.value}, currentProject = `, currentProject.value)
|
||||||
|
|
||||||
// Set the current project to the one we're about to load so that the title is already shown at the top
|
// Set the current project to the one we're about to load so that the title is already shown at the top
|
||||||
loadedProjectId.value = 0
|
loadedProjectId.value = 0
|
||||||
@ -149,31 +125,46 @@ watch(
|
|||||||
const loadedProject = await projectService.value.get(project)
|
const loadedProject = await projectService.value.get(project)
|
||||||
baseStore.handleSetCurrentProject({project: loadedProject})
|
baseStore.handleSetCurrentProject({project: loadedProject})
|
||||||
} finally {
|
} finally {
|
||||||
loadedProjectId.value = props.projectId
|
loadedProjectId.value = projectId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{immediate: true},
|
{immediate: true},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function getViewTitle(view: IProjectView) {
|
||||||
|
switch (view.title) {
|
||||||
|
case 'List':
|
||||||
|
return t('project.list.title')
|
||||||
|
case 'Gantt':
|
||||||
|
return t('project.gantt.title')
|
||||||
|
case 'Table':
|
||||||
|
return t('project.table.title')
|
||||||
|
case 'Kanban':
|
||||||
|
return t('project.kanban.title')
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.title
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.switch-view-container {
|
.switch-view-container {
|
||||||
@media screen and (max-width: $tablet) {
|
@media screen and (max-width: $tablet) {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-view {
|
.switch-view {
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
height: $switch-view-height;
|
height: $switch-view-height;
|
||||||
margin: 0 auto 1rem;
|
margin: 0 auto 1rem;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-view-button {
|
.switch-view-button {
|
||||||
@ -201,7 +192,7 @@ watch(
|
|||||||
|
|
||||||
// FIXME: this should be in notification and set via a prop
|
// FIXME: this should be in notification and set via a prop
|
||||||
.is-archived .notification.is-warning {
|
.is-archived .notification.is-warning {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-title-print {
|
.project-title-print {
|
||||||
|
@ -21,13 +21,16 @@ import {
|
|||||||
LABEL_FIELDS,
|
LABEL_FIELDS,
|
||||||
} from '@/helpers/filters'
|
} from '@/helpers/filters'
|
||||||
import {useDebounceFn} from '@vueuse/core'
|
import {useDebounceFn} from '@vueuse/core'
|
||||||
|
import {createRandomID} from '@/helpers/randomId'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
modelValue,
|
modelValue,
|
||||||
projectId,
|
projectId,
|
||||||
|
inputLabel = undefined,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
modelValue: string,
|
modelValue: string,
|
||||||
projectId?: number,
|
projectId?: number,
|
||||||
|
inputLabel?: string,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'blur'])
|
const emit = defineEmits(['update:modelValue', 'blur'])
|
||||||
@ -38,6 +41,8 @@ const {
|
|||||||
height,
|
height,
|
||||||
} = useAutoHeightTextarea(filterQuery)
|
} = useAutoHeightTextarea(filterQuery)
|
||||||
|
|
||||||
|
const id = ref(createRandomID())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => modelValue,
|
() => modelValue,
|
||||||
() => {
|
() => {
|
||||||
@ -246,7 +251,12 @@ const blurDebounced = useDebounceFn(() => emit('blur'), 500)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ $t('filters.query.title') }}</label>
|
<label
|
||||||
|
class="label"
|
||||||
|
:for="id"
|
||||||
|
>
|
||||||
|
{{ inputLabel ?? $t('filters.query.title') }}
|
||||||
|
</label>
|
||||||
<AutocompleteDropdown
|
<AutocompleteDropdown
|
||||||
:options="autocompleteResults"
|
:options="autocompleteResults"
|
||||||
@blur="filterInput?.blur()"
|
@blur="filterInput?.blur()"
|
||||||
@ -257,10 +267,10 @@ const blurDebounced = useDebounceFn(() => emit('blur'), 500)
|
|||||||
>
|
>
|
||||||
<div class="control filter-input">
|
<div class="control filter-input">
|
||||||
<textarea
|
<textarea
|
||||||
|
:id
|
||||||
ref="filterInput"
|
ref="filterInput"
|
||||||
v-model="filterQuery"
|
v-model="filterQuery"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</Fancycheckbox>
|
</Fancycheckbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FilterInputDocs/>
|
<FilterInputDocs />
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-if="hasFooter"
|
v-if="hasFooter"
|
||||||
|
@ -47,6 +47,12 @@
|
|||||||
>
|
>
|
||||||
{{ $t('menu.edit') }}
|
{{ $t('menu.edit') }}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
:to="{ name: 'project.settings.views', params: { projectId: project.id } }"
|
||||||
|
icon="eye"
|
||||||
|
>
|
||||||
|
{{ $t('menu.views') }}
|
||||||
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-if="backgroundsEnabled"
|
v-if="backgroundsEnabled"
|
||||||
:to="{ name: 'project.settings.background', params: { projectId: project.id } }"
|
:to="{ name: 'project.settings.background', params: { projectId: project.id } }"
|
||||||
|
@ -1,8 +1,24 @@
|
|||||||
|
<!-- Vikunja is a to-do list application to facilitate your life. -->
|
||||||
|
<!-- Copyright 2018-present Vikunja and contributors. All rights reserved. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- This program is free software: you can redistribute it and/or modify -->
|
||||||
|
<!-- it under the terms of the GNU Affero General Public Licensee as published by -->
|
||||||
|
<!-- the Free Software Foundation, either version 3 of the License, or -->
|
||||||
|
<!-- (at your option) any later version. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- This program is distributed in the hope that it will be useful, -->
|
||||||
|
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
|
||||||
|
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
|
||||||
|
<!-- GNU Affero General Public Licensee for more details. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- You should have received a copy of the GNU Affero General Public Licensee -->
|
||||||
|
<!-- along with this program. If not, see <https://www.gnu.org/licenses/>. -->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ProjectWrapper
|
<ProjectWrapper
|
||||||
class="project-gantt"
|
class="project-gantt"
|
||||||
:project-id="filters.projectId"
|
:project-id="filters.projectId"
|
||||||
view-name="gantt"
|
:view
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<card :has-content="false">
|
<card :has-content="false">
|
||||||
@ -87,15 +103,19 @@ import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
|||||||
import TaskForm from '@/components/tasks/TaskForm.vue'
|
import TaskForm from '@/components/tasks/TaskForm.vue'
|
||||||
|
|
||||||
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
|
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
|
||||||
import {useGanttFilters} from './helpers/useGanttFilters'
|
import {useGanttFilters} from '../../../views/project/helpers/useGanttFilters'
|
||||||
import {RIGHTS} from '@/constants/rights'
|
import {RIGHTS} from '@/constants/rights'
|
||||||
|
|
||||||
import type {DateISO} from '@/types/DateISO'
|
import type {DateISO} from '@/types/DateISO'
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
type Options = Flatpickr.Options.Options
|
type Options = Flatpickr.Options.Options
|
||||||
|
|
||||||
const props = defineProps<{route: RouteLocationNormalized}>()
|
const props = defineProps<{
|
||||||
|
route: RouteLocationNormalized
|
||||||
|
viewId: IProjectView['id']
|
||||||
|
}>()
|
||||||
|
|
||||||
const GanttChart = createAsyncComponent(() => import('@/components/tasks/GanttChart.vue'))
|
const GanttChart = createAsyncComponent(() => import('@/components/tasks/GanttChart.vue'))
|
||||||
|
|
||||||
@ -111,7 +131,7 @@ const {
|
|||||||
isLoading,
|
isLoading,
|
||||||
addTask,
|
addTask,
|
||||||
updateTask,
|
updateTask,
|
||||||
} = useGanttFilters(route)
|
} = useGanttFilters(route, props.viewId)
|
||||||
|
|
||||||
const DEFAULT_DATE_RANGE_DAYS = 7
|
const DEFAULT_DATE_RANGE_DAYS = 7
|
||||||
|
|
@ -2,7 +2,7 @@
|
|||||||
<ProjectWrapper
|
<ProjectWrapper
|
||||||
class="project-kanban"
|
class="project-kanban"
|
||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
view-name="kanban"
|
:view-id
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
@ -277,7 +277,6 @@ import {RIGHTS as Rights} from '@/constants/rights'
|
|||||||
import BucketModel from '@/models/bucket'
|
import BucketModel from '@/models/bucket'
|
||||||
|
|
||||||
import type {IBucket} from '@/modelTypes/IBucket'
|
import type {IBucket} from '@/modelTypes/IBucket'
|
||||||
import type {IProject} from '@/modelTypes/IProject'
|
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
@ -301,11 +300,17 @@ import {isSavedFilter} from '@/services/savedFilter'
|
|||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
import type {TaskFilterParams} from '@/services/taskCollection'
|
import type {TaskFilterParams} from '@/services/taskCollection'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
import TaskPositionService from '@/services/taskPosition'
|
||||||
|
import TaskPositionModel from '@/models/taskPosition'
|
||||||
|
import {i18n} from '@/i18n'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
projectId = undefined,
|
projectId,
|
||||||
|
viewId,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
projectId: number,
|
projectId: number,
|
||||||
|
viewId: IProjectView['id'],
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const DRAG_OPTIONS = {
|
const DRAG_OPTIONS = {
|
||||||
@ -325,6 +330,7 @@ const baseStore = useBaseStore()
|
|||||||
const kanbanStore = useKanbanStore()
|
const kanbanStore = useKanbanStore()
|
||||||
const taskStore = useTaskStore()
|
const taskStore = useTaskStore()
|
||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
|
const taskPositionService = ref(new TaskPositionService())
|
||||||
|
|
||||||
const taskContainerRefs = ref<{ [id: IBucket['id']]: HTMLElement }>({})
|
const taskContainerRefs = ref<{ [id: IBucket['id']]: HTMLElement }>({})
|
||||||
const bucketLimitInputRef = ref<HTMLInputElement | null>(null)
|
const bucketLimitInputRef = ref<HTMLInputElement | null>(null)
|
||||||
@ -363,7 +369,7 @@ const params = ref<TaskFilterParams>({
|
|||||||
const getTaskDraggableTaskComponentData = computed(() => (bucket: IBucket) => {
|
const getTaskDraggableTaskComponentData = computed(() => (bucket: IBucket) => {
|
||||||
return {
|
return {
|
||||||
ref: (el: HTMLElement) => setTaskContainerRef(bucket.id, el),
|
ref: (el: HTMLElement) => setTaskContainerRef(bucket.id, el),
|
||||||
onScroll: (event: Event) => handleTaskContainerScroll(bucket.id, bucket.projectId, event.target as HTMLElement),
|
onScroll: (event: Event) => handleTaskContainerScroll(bucket.id, event.target as HTMLElement),
|
||||||
type: 'transition-group',
|
type: 'transition-group',
|
||||||
name: !drag.value ? 'move-card' : null,
|
name: !drag.value ? 'move-card' : null,
|
||||||
class: [
|
class: [
|
||||||
@ -387,19 +393,20 @@ const project = computed(() => projectId ? projectStore.projects[projectId] : nu
|
|||||||
const buckets = computed(() => kanbanStore.buckets)
|
const buckets = computed(() => kanbanStore.buckets)
|
||||||
const loading = computed(() => kanbanStore.isLoading)
|
const loading = computed(() => kanbanStore.isLoading)
|
||||||
|
|
||||||
const taskLoading = computed(() => taskStore.isLoading)
|
const taskLoading = computed(() => taskStore.isLoading || taskPositionService.value.loading)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => ({
|
() => ({
|
||||||
params: params.value,
|
params: params.value,
|
||||||
projectId,
|
projectId,
|
||||||
|
viewId,
|
||||||
}),
|
}),
|
||||||
({params}) => {
|
({params}) => {
|
||||||
if (projectId === undefined || Number(projectId) === 0) {
|
if (projectId === undefined || Number(projectId) === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
collapsedBuckets.value = getCollapsedBucketState(projectId)
|
collapsedBuckets.value = getCollapsedBucketState(projectId)
|
||||||
kanbanStore.loadBucketsForProject({projectId, params})
|
kanbanStore.loadBucketsForProject(projectId, viewId, params)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@ -412,7 +419,7 @@ function setTaskContainerRef(id: IBucket['id'], el: HTMLElement) {
|
|||||||
taskContainerRefs.value[id] = el
|
taskContainerRefs.value[id] = el
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTaskContainerScroll(id: IBucket['id'], projectId: IProject['id'], el: HTMLElement) {
|
function handleTaskContainerScroll(id: IBucket['id'], el: HTMLElement) {
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -424,6 +431,7 @@ function handleTaskContainerScroll(id: IBucket['id'], projectId: IProject['id'],
|
|||||||
|
|
||||||
kanbanStore.loadNextTasksForBucket(
|
kanbanStore.loadNextTasksForBucket(
|
||||||
projectId,
|
projectId,
|
||||||
|
viewId,
|
||||||
params.value,
|
params.value,
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
@ -473,7 +481,7 @@ async function updateTaskPosition(e) {
|
|||||||
|
|
||||||
const newTask = klona(task) // cloning the task to avoid pinia store manipulation
|
const newTask = klona(task) // cloning the task to avoid pinia store manipulation
|
||||||
newTask.bucketId = newBucket.id
|
newTask.bucketId = newBucket.id
|
||||||
newTask.kanbanPosition = calculateItemPosition(
|
const position = calculateItemPosition(
|
||||||
taskBefore !== null ? taskBefore.kanbanPosition : null,
|
taskBefore !== null ? taskBefore.kanbanPosition : null,
|
||||||
taskAfter !== null ? taskAfter.kanbanPosition : null,
|
taskAfter !== null ? taskAfter.kanbanPosition : null,
|
||||||
)
|
)
|
||||||
@ -483,6 +491,8 @@ async function updateTaskPosition(e) {
|
|||||||
) {
|
) {
|
||||||
newTask.done = project.value?.doneBucketId === newBucket.id
|
newTask.done = project.value?.doneBucketId === newBucket.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bucketHasChanged = false
|
||||||
if (
|
if (
|
||||||
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
||||||
newBucket.id !== oldBucket.id
|
newBucket.id !== oldBucket.id
|
||||||
@ -495,10 +505,20 @@ async function updateTaskPosition(e) {
|
|||||||
...newBucket,
|
...newBucket,
|
||||||
count: newBucket.count + 1,
|
count: newBucket.count + 1,
|
||||||
})
|
})
|
||||||
|
bucketHasChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await taskStore.update(newTask)
|
const newPosition = new TaskPositionModel({
|
||||||
|
position,
|
||||||
|
projectViewId: viewId,
|
||||||
|
taskId: newTask.id,
|
||||||
|
})
|
||||||
|
await taskPositionService.value.update(newPosition)
|
||||||
|
|
||||||
|
if(bucketHasChanged) {
|
||||||
|
await taskStore.update(newTask)
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the first and second task don't both get position 0 assigned
|
// Make sure the first and second task don't both get position 0 assigned
|
||||||
if (newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
|
if (newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
|
||||||
@ -556,6 +576,7 @@ async function createNewBucket() {
|
|||||||
await kanbanStore.createBucket(new BucketModel({
|
await kanbanStore.createBucket(new BucketModel({
|
||||||
title: newBucketTitle.value,
|
title: newBucketTitle.value,
|
||||||
projectId: project.value.id,
|
projectId: project.value.id,
|
||||||
|
projectViewId: viewId,
|
||||||
}))
|
}))
|
||||||
newBucketTitle.value = ''
|
newBucketTitle.value = ''
|
||||||
}
|
}
|
||||||
@ -575,6 +596,7 @@ async function deleteBucket() {
|
|||||||
bucket: new BucketModel({
|
bucket: new BucketModel({
|
||||||
id: bucketToDelete.value,
|
id: bucketToDelete.value,
|
||||||
projectId: project.value.id,
|
projectId: project.value.id,
|
||||||
|
projectViewId: viewId,
|
||||||
}),
|
}),
|
||||||
params: params.value,
|
params: params.value,
|
||||||
})
|
})
|
||||||
@ -593,10 +615,19 @@ async function focusBucketTitle(e: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
|
async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
|
||||||
await kanbanStore.updateBucketTitle({
|
|
||||||
|
const bucket = kanbanStore.getBucketById(bucketId)
|
||||||
|
if (bucket?.title === bucketTitle) {
|
||||||
|
bucketTitleEditable.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await kanbanStore.updateBucket({
|
||||||
id: bucketId,
|
id: bucketId,
|
||||||
title: bucketTitle,
|
title: bucketTitle,
|
||||||
|
projectId,
|
||||||
})
|
})
|
||||||
|
success({message: i18n.global.t('project.kanban.bucketTitleSavedSuccess')})
|
||||||
bucketTitleEditable.value = false
|
bucketTitleEditable.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,6 +647,7 @@ function updateBucketPosition(e: { newIndex: number }) {
|
|||||||
|
|
||||||
kanbanStore.updateBucket({
|
kanbanStore.updateBucket({
|
||||||
id: bucket.id,
|
id: bucket.id,
|
||||||
|
projectId,
|
||||||
position: calculateItemPosition(
|
position: calculateItemPosition(
|
||||||
bucketBefore !== null ? bucketBefore.position : null,
|
bucketBefore !== null ? bucketBefore.position : null,
|
||||||
bucketAfter !== null ? bucketAfter.position : null,
|
bucketAfter !== null ? bucketAfter.position : null,
|
||||||
@ -630,6 +662,7 @@ async function saveBucketLimit(bucketId: IBucket['id'], limit: number) {
|
|||||||
|
|
||||||
await kanbanStore.updateBucket({
|
await kanbanStore.updateBucket({
|
||||||
...kanbanStore.getBucketById(bucketId),
|
...kanbanStore.getBucketById(bucketId),
|
||||||
|
projectId,
|
||||||
limit,
|
limit,
|
||||||
})
|
})
|
||||||
success({message: t('project.kanban.bucketLimitSavedSuccess')})
|
success({message: t('project.kanban.bucketLimitSavedSuccess')})
|
@ -2,7 +2,7 @@
|
|||||||
<ProjectWrapper
|
<ProjectWrapper
|
||||||
class="project-list"
|
class="project-list"
|
||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
view-name="project"
|
:view-id
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
@ -114,14 +114,18 @@ import type {ITask} from '@/modelTypes/ITask'
|
|||||||
import {isSavedFilter} from '@/services/savedFilter'
|
import {isSavedFilter} from '@/services/savedFilter'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useTaskStore} from '@/stores/tasks'
|
|
||||||
|
|
||||||
import type {IProject} from '@/modelTypes/IProject'
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
import TaskPositionService from '@/services/taskPosition'
|
||||||
|
import TaskPositionModel from '@/models/taskPosition'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
projectId,
|
projectId,
|
||||||
|
viewId,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
projectId: IProject['id'],
|
projectId: IProject['id'],
|
||||||
|
viewId: IProjectView['id'],
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const ctaVisible = ref(false)
|
const ctaVisible = ref(false)
|
||||||
@ -140,7 +144,9 @@ const {
|
|||||||
loadTasks,
|
loadTasks,
|
||||||
params,
|
params,
|
||||||
sortByParam,
|
sortByParam,
|
||||||
} = useTaskList(() => projectId, {position: 'asc'})
|
} = useTaskList(() => projectId, () => viewId, {position: 'asc'})
|
||||||
|
|
||||||
|
const taskPositionService = ref(new TaskPositionService())
|
||||||
|
|
||||||
const tasks = ref<ITask[]>([])
|
const tasks = ref<ITask[]>([])
|
||||||
watch(
|
watch(
|
||||||
@ -182,7 +188,6 @@ const firstNewPosition = computed(() => {
|
|||||||
return calculateItemPosition(null, tasks.value[0].position)
|
return calculateItemPosition(null, tasks.value[0].position)
|
||||||
})
|
})
|
||||||
|
|
||||||
const taskStore = useTaskStore()
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const project = computed(() => baseStore.currentProject)
|
const project = computed(() => baseStore.currentProject)
|
||||||
|
|
||||||
@ -231,13 +236,17 @@ async function saveTaskPosition(e) {
|
|||||||
const taskBefore = tasks.value[e.newIndex - 1] ?? null
|
const taskBefore = tasks.value[e.newIndex - 1] ?? null
|
||||||
const taskAfter = tasks.value[e.newIndex + 1] ?? null
|
const taskAfter = tasks.value[e.newIndex + 1] ?? null
|
||||||
|
|
||||||
const newTask = {
|
const position = calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null)
|
||||||
...task,
|
|
||||||
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTask = await taskStore.update(newTask)
|
await taskPositionService.value.update(new TaskPositionModel({
|
||||||
tasks.value[e.newIndex] = updatedTask
|
position,
|
||||||
|
projectViewId: viewId,
|
||||||
|
taskId: task.id,
|
||||||
|
}))
|
||||||
|
tasks.value[e.newIndex] = {
|
||||||
|
...task,
|
||||||
|
position,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareFiltersAndLoadTasks() {
|
function prepareFiltersAndLoadTasks() {
|
@ -2,7 +2,7 @@
|
|||||||
<ProjectWrapper
|
<ProjectWrapper
|
||||||
class="project-table"
|
class="project-table"
|
||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
view-name="table"
|
:view-id
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
@ -289,11 +289,14 @@ import {useTaskList} from '@/composables/useTaskList'
|
|||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
import type {IProject} from '@/modelTypes/IProject'
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
|
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
projectId,
|
projectId,
|
||||||
|
viewId,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
projectId: IProject['id'],
|
projectId: IProject['id'],
|
||||||
|
viewId: IProjectView['id'],
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const ACTIVE_COLUMNS_DEFAULT = {
|
const ACTIVE_COLUMNS_DEFAULT = {
|
||||||
@ -320,7 +323,7 @@ const SORT_BY_DEFAULT: SortBy = {
|
|||||||
const activeColumns = useStorage('tableViewColumns', {...ACTIVE_COLUMNS_DEFAULT})
|
const activeColumns = useStorage('tableViewColumns', {...ACTIVE_COLUMNS_DEFAULT})
|
||||||
const sortBy = useStorage<SortBy>('tableViewSortBy', {...SORT_BY_DEFAULT})
|
const sortBy = useStorage<SortBy>('tableViewSortBy', {...SORT_BY_DEFAULT})
|
||||||
|
|
||||||
const taskList = useTaskList(() => projectId, sortBy.value)
|
const taskList = useTaskList(() => projectId, () => viewId, sortBy.value)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
180
frontend/src/components/project/views/viewEditForm.vue
Normal file
180
frontend/src/components/project/views/viewEditForm.vue
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
import XButton from '@/components/input/button.vue'
|
||||||
|
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
||||||
|
import {ref} from 'vue'
|
||||||
|
|
||||||
|
const model = defineModel<IProjectView>()
|
||||||
|
const titleValid = ref(true)
|
||||||
|
function validateTitle() {
|
||||||
|
titleValid.value = model.value.title !== ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<div class="field">
|
||||||
|
<label
|
||||||
|
class="label"
|
||||||
|
for="title"
|
||||||
|
>
|
||||||
|
{{ $t('project.views.title') }}
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
v-model="model.title"
|
||||||
|
v-focus
|
||||||
|
class="input"
|
||||||
|
:placeholder="$t('project.share.links.namePlaceholder')"
|
||||||
|
@blur="validateTitle"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="!titleValid"
|
||||||
|
class="help is-danger"
|
||||||
|
>
|
||||||
|
{{ $t('project.views.titleRequired') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label
|
||||||
|
class="label"
|
||||||
|
for="kind"
|
||||||
|
>
|
||||||
|
{{ $t('project.views.kind') }}
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select
|
||||||
|
id="kind"
|
||||||
|
v-model="model.viewKind"
|
||||||
|
>
|
||||||
|
<option value="list">
|
||||||
|
{{ $t('project.list.title') }}
|
||||||
|
</option>
|
||||||
|
<option value="gantt">
|
||||||
|
{{ $t('project.gantt.title') }}
|
||||||
|
</option>
|
||||||
|
<option value="table">
|
||||||
|
{{ $t('project.table.title') }}
|
||||||
|
</option>
|
||||||
|
<option value="kanban">
|
||||||
|
{{ $t('project.kanban.title') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FilterInput
|
||||||
|
v-model="model.filter"
|
||||||
|
:input-label="$t('project.views.filter')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="model.viewKind === 'kanban'"
|
||||||
|
class="field"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="label"
|
||||||
|
for="configMode"
|
||||||
|
>
|
||||||
|
{{ $t('project.views.bucketConfigMode') }}
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select
|
||||||
|
id="configMode"
|
||||||
|
v-model="model.bucketConfigurationMode"
|
||||||
|
>
|
||||||
|
<option value="manual">
|
||||||
|
{{ $t('project.views.bucketConfigManual') }}
|
||||||
|
</option>
|
||||||
|
<option value="filter">
|
||||||
|
{{ $t('project.views.filter') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="model.viewKind === 'kanban' && model.bucketConfigurationMode === 'filter'"
|
||||||
|
class="field"
|
||||||
|
>
|
||||||
|
<label class="label">
|
||||||
|
{{ $t('project.views.bucketConfig') }}
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<div
|
||||||
|
v-for="(b, index) in model.bucketConfiguration"
|
||||||
|
:key="'bucket_'+index"
|
||||||
|
class="filter-bucket"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="is-danger"
|
||||||
|
@click.prevent="() => model.bucketConfiguration.splice(index, 1)"
|
||||||
|
>
|
||||||
|
<icon icon="trash-alt" />
|
||||||
|
</button>
|
||||||
|
<div class="filter-bucket-form">
|
||||||
|
<div class="field">
|
||||||
|
<label
|
||||||
|
class="label"
|
||||||
|
:for="'bucket_'+index+'_title'"
|
||||||
|
>
|
||||||
|
{{ $t('project.views.title') }}
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<input
|
||||||
|
:id="'bucket_'+index+'_title'"
|
||||||
|
v-model="model.bucketConfiguration[index].title"
|
||||||
|
class="input"
|
||||||
|
:placeholder="$t('project.share.links.namePlaceholder')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FilterInput
|
||||||
|
v-model="model.bucketConfiguration[index].filter"
|
||||||
|
:input-label="$t('project.views.filter')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="is-flex is-justify-content-end">
|
||||||
|
<XButton
|
||||||
|
variant="secondary"
|
||||||
|
icon="plus"
|
||||||
|
@click="() => model.bucketConfiguration.push({title: '', filter: ''})"
|
||||||
|
>
|
||||||
|
{{ $t('project.kanban.addBucket') }}
|
||||||
|
</XButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.filter-bucket {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--danger);
|
||||||
|
padding-right: .75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-form {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
padding: .5rem;
|
||||||
|
border: 1px solid var(--grey-200);
|
||||||
|
border-radius: $radius;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -173,11 +173,11 @@
|
|||||||
<div class="select">
|
<div class="select">
|
||||||
<select v-model="selectedView[s.id]">
|
<select v-model="selectedView[s.id]">
|
||||||
<option
|
<option
|
||||||
v-for="(title, key) in availableViews"
|
v-for="(view) in availableViews"
|
||||||
:key="key"
|
:key="view.id"
|
||||||
:value="key"
|
:value="view.id"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ view.title }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -230,9 +230,9 @@ import LinkShareService from '@/services/linkShare'
|
|||||||
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import {getDisplayName} from '@/models/user'
|
import {getDisplayName} from '@/models/user'
|
||||||
import type {ProjectView} from '@/types/ProjectView'
|
|
||||||
import {PROJECT_VIEWS} from '@/types/ProjectView'
|
|
||||||
import {useConfigStore} from '@/stores/config'
|
import {useConfigStore} from '@/stores/config'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
projectId: {
|
projectId: {
|
||||||
@ -252,17 +252,13 @@ const showDeleteModal = ref(false)
|
|||||||
const linkIdToDelete = ref(0)
|
const linkIdToDelete = ref(0)
|
||||||
const showNewForm = ref(false)
|
const showNewForm = ref(false)
|
||||||
|
|
||||||
type SelectedViewMapper = Record<IProject['id'], ProjectView>
|
type SelectedViewMapper = Record<IProject['id'], IProjectView['id']>
|
||||||
|
|
||||||
const selectedView = ref<SelectedViewMapper>({})
|
const selectedView = ref<SelectedViewMapper>({})
|
||||||
|
|
||||||
const availableViews = computed<Record<ProjectView, string>>(() => ({
|
const projectStore = useProjectStore()
|
||||||
list: t('project.list.title'),
|
|
||||||
gantt: t('project.gantt.title'),
|
|
||||||
table: t('project.table.title'),
|
|
||||||
kanban: t('project.kanban.title'),
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
const availableViews = computed<IProjectView[]>(() => projectStore.projects[props.projectId]?.views || [])
|
||||||
const copy = useCopyToClipboard()
|
const copy = useCopyToClipboard()
|
||||||
watch(
|
watch(
|
||||||
() => props.projectId,
|
() => props.projectId,
|
||||||
@ -281,7 +277,7 @@ async function load(projectId: IProject['id']) {
|
|||||||
|
|
||||||
const links = await linkShareService.getAll({projectId})
|
const links = await linkShareService.getAll({projectId})
|
||||||
links.forEach((l: ILinkShare) => {
|
links.forEach((l: ILinkShare) => {
|
||||||
selectedView.value[l.id] = 'list'
|
selectedView.value[l.id] = availableViews.value[0].id
|
||||||
})
|
})
|
||||||
linkShares.value = links
|
linkShares.value = links
|
||||||
}
|
}
|
||||||
@ -315,8 +311,8 @@ async function remove(projectId: IProject['id']) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getShareLink(hash: string, view: ProjectView = PROJECT_VIEWS.LIST) {
|
function getShareLink(hash: string, viewId: IProjectView['id']) {
|
||||||
return frontendUrl.value + 'share/' + hash + '/auth?view=' + view
|
return frontendUrl.value + 'share/' + hash + '/auth?view=' + viewId
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
v-if="showProject && typeof project !== 'undefined'"
|
v-if="showProject && typeof project !== 'undefined'"
|
||||||
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
|
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
|
||||||
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
|
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
|
||||||
class="task-project mr-1"
|
class="task-project mr-1"
|
||||||
:class="{'mr-2': task.hexColor !== ''}"
|
:class="{'mr-2': task.hexColor !== ''}"
|
||||||
>
|
>
|
||||||
@ -136,7 +136,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
v-if="showProjectSeparately"
|
v-if="showProjectSeparately"
|
||||||
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
|
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
|
||||||
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
|
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
|
||||||
class="task-project"
|
class="task-project"
|
||||||
>
|
>
|
||||||
{{ project.title }}
|
{{ project.title }}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import {computed, shallowRef, watchEffect, h, type VNode} from 'vue'
|
import {computed, h, shallowRef, type VNode, watchEffect} from 'vue'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
export function useRouteWithModal() {
|
export function useRouteWithModal() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
|
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
|
||||||
const routeWithModal = computed(() => {
|
const routeWithModal = computed(() => {
|
||||||
return backdropView.value
|
return backdropView.value
|
||||||
@ -29,7 +31,7 @@ export function useRouteWithModal() {
|
|||||||
if (routePropsOption === true) {
|
if (routePropsOption === true) {
|
||||||
routeProps = route.params
|
routeProps = route.params
|
||||||
} else {
|
} else {
|
||||||
if(typeof routePropsOption === 'function') {
|
if (typeof routePropsOption === 'function') {
|
||||||
routeProps = routePropsOption(route)
|
routeProps = routePropsOption(route)
|
||||||
} else {
|
} else {
|
||||||
routeProps = routePropsOption
|
routeProps = routePropsOption
|
||||||
@ -60,12 +62,23 @@ export function useRouteWithModal() {
|
|||||||
// If the current project was changed because the user moved the currently opened task while coming from kanban,
|
// If the current project was changed because the user moved the currently opened task while coming from kanban,
|
||||||
// we need to reflect that change in the route when they close the task modal.
|
// we need to reflect that change in the route when they close the task modal.
|
||||||
// The last route is only available as resolved string, therefore we need to use a regex for matching here
|
// The last route is only available as resolved string, therefore we need to use a regex for matching here
|
||||||
const kanbanRouteMatch = new RegExp('\\/projects\\/\\d+\\/kanban', 'g')
|
const routeMatch = new RegExp('\\/projects\\/\\d+\\/(\\d+)', 'g')
|
||||||
const kanbanRouter = {name: 'project.kanban', params: {projectId: baseStore.currentProject?.id}}
|
const match = routeMatch.exec(historyState.value.back)
|
||||||
if (kanbanRouteMatch.test(historyState.value.back)
|
if (match !== null && baseStore.currentProject) {
|
||||||
&& baseStore.currentProject
|
let viewId: string | number = match[1]
|
||||||
&& historyState.value.back !== router.resolve(kanbanRouter).fullPath) {
|
|
||||||
router.push(kanbanRouter)
|
if (!viewId) {
|
||||||
|
viewId = projectStore.projects[baseStore.currentProject?.id].views[0]?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRoute = {
|
||||||
|
name: 'project.view',
|
||||||
|
params: {
|
||||||
|
projectId: baseStore.currentProject?.id,
|
||||||
|
viewId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
router.push(newRoute)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import type {ITask} from '@/modelTypes/ITask'
|
|||||||
import {error} from '@/message'
|
import {error} from '@/message'
|
||||||
import type {IProject} from '@/modelTypes/IProject'
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
export type Order = 'asc' | 'desc' | 'none'
|
export type Order = 'asc' | 'desc' | 'none'
|
||||||
|
|
||||||
@ -54,9 +55,14 @@ const SORT_BY_DEFAULT: SortBy = {
|
|||||||
/**
|
/**
|
||||||
* This mixin provides a base set of methods and properties to get tasks.
|
* This mixin provides a base set of methods and properties to get tasks.
|
||||||
*/
|
*/
|
||||||
export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sortByDefault: SortBy = SORT_BY_DEFAULT) {
|
export function useTaskList(
|
||||||
|
projectIdGetter: ComputedGetter<IProject['id']>,
|
||||||
|
projectViewIdGetter: ComputedGetter<IProjectView['id']>,
|
||||||
|
sortByDefault: SortBy = SORT_BY_DEFAULT,
|
||||||
|
) {
|
||||||
|
|
||||||
const projectId = computed(() => projectIdGetter())
|
const projectId = computed(() => projectIdGetter())
|
||||||
|
const projectViewId = computed(() => projectViewIdGetter())
|
||||||
|
|
||||||
const params = ref<TaskFilterParams>({...getDefaultTaskFilterParams()})
|
const params = ref<TaskFilterParams>({...getDefaultTaskFilterParams()})
|
||||||
|
|
||||||
@ -87,7 +93,10 @@ export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sor
|
|||||||
|
|
||||||
const getAllTasksParams = computed(() => {
|
const getAllTasksParams = computed(() => {
|
||||||
return [
|
return [
|
||||||
{projectId: projectId.value},
|
{
|
||||||
|
projectId: projectId.value,
|
||||||
|
viewId: projectViewId.value,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...allParams.value,
|
...allParams.value,
|
||||||
filter_timezone: authStore.settings.timezone,
|
filter_timezone: authStore.settings.timezone,
|
||||||
|
@ -1,61 +1,14 @@
|
|||||||
import type { RouteRecordName } from 'vue-router'
|
|
||||||
import router from '@/router'
|
|
||||||
|
|
||||||
import type {IProject} from '@/modelTypes/IProject'
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
|
||||||
export type ProjectRouteName = Extract<RouteRecordName, string>
|
export type ProjectViewSettings = Record<IProject['id'], number>
|
||||||
export type ProjectViewSettings = Record<
|
|
||||||
IProject['id'],
|
|
||||||
Extract<RouteRecordName, ProjectRouteName>
|
|
||||||
>
|
|
||||||
|
|
||||||
const SETTINGS_KEY_PROJECT_VIEW = 'projectView'
|
const SETTINGS_KEY_PROJECT_VIEW = 'projectView'
|
||||||
|
|
||||||
// TODO: remove migration when releasing 1.0
|
|
||||||
type ListViewSettings = ProjectViewSettings
|
|
||||||
const SETTINGS_KEY_DEPRECATED_LIST_VIEW = 'listView'
|
|
||||||
function migrateStoredProjectRouteSettings() {
|
|
||||||
try {
|
|
||||||
const listViewSettingsString = localStorage.getItem(SETTINGS_KEY_DEPRECATED_LIST_VIEW)
|
|
||||||
if (listViewSettingsString === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// A) the first version stored one setting for all lists in a string
|
|
||||||
if (listViewSettingsString.startsWith('list.')) {
|
|
||||||
const projectView = listViewSettingsString.replace('list.', 'project.')
|
|
||||||
|
|
||||||
if (!router.hasRoute(projectView)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return projectView as RouteRecordName
|
|
||||||
}
|
|
||||||
|
|
||||||
// B) the last version used a 'list.' prefix
|
|
||||||
const listViewSettings: ListViewSettings = JSON.parse(listViewSettingsString)
|
|
||||||
|
|
||||||
const projectViewSettingEntries = Object.entries(listViewSettings).map(([id, value]) => {
|
|
||||||
return [id, value.replace('list.', 'project.')]
|
|
||||||
})
|
|
||||||
const projectViewSettings = Object.fromEntries(projectViewSettingEntries)
|
|
||||||
|
|
||||||
localStorage.setItem(SETTINGS_KEY_PROJECT_VIEW, JSON.stringify(projectViewSettings))
|
|
||||||
} catch(e) {
|
|
||||||
//
|
|
||||||
} finally {
|
|
||||||
localStorage.removeItem(SETTINGS_KEY_DEPRECATED_LIST_VIEW)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the current project view to local storage
|
* Save the current project view to local storage
|
||||||
*/
|
*/
|
||||||
export function saveProjectView(projectId: IProject['id'], routeName: string) {
|
export function saveProjectView(projectId: IProject['id'], viewId: number) {
|
||||||
if (routeName.includes('settings.')) {
|
if (!projectId || !viewId) {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!projectId) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,30 +24,19 @@ export function saveProjectView(projectId: IProject['id'], routeName: string) {
|
|||||||
projectViewSettings = savedProjectViewSettings
|
projectViewSettings = savedProjectViewSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
projectViewSettings[projectId] = routeName
|
projectViewSettings[projectId] = viewId
|
||||||
localStorage.setItem(SETTINGS_KEY_PROJECT_VIEW, JSON.stringify(projectViewSettings))
|
localStorage.setItem(SETTINGS_KEY_PROJECT_VIEW, JSON.stringify(projectViewSettings))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getProjectView = (projectId: IProject['id']) => {
|
export function getProjectViewId(projectId: IProject['id']): number {
|
||||||
// TODO: remove migration when releasing 1.0
|
const projectViewSettingsString = localStorage.getItem(SETTINGS_KEY_PROJECT_VIEW)
|
||||||
const migratedProjectView = migrateStoredProjectRouteSettings()
|
if (!projectViewSettingsString) {
|
||||||
|
return 0
|
||||||
if (migratedProjectView !== undefined && router.hasRoute(migratedProjectView)) {
|
|
||||||
return migratedProjectView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const projectViewSettings = JSON.parse(projectViewSettingsString) as ProjectViewSettings
|
||||||
const projectViewSettingsString = localStorage.getItem(SETTINGS_KEY_PROJECT_VIEW)
|
if (isNaN(projectViewSettings[projectId])) {
|
||||||
if (!projectViewSettingsString) {
|
return 0
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectViewSettings = JSON.parse(projectViewSettingsString) as ProjectViewSettings
|
|
||||||
if (!router.hasRoute(projectViewSettings[projectId])) {
|
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
return projectViewSettings[projectId]
|
|
||||||
} catch (e) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return projectViewSettings[projectId]
|
||||||
}
|
}
|
@ -381,6 +381,22 @@
|
|||||||
"secret": "Secret",
|
"secret": "Secret",
|
||||||
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
|
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
|
||||||
"secretDocs": "Check out the docs for more details about how to use secrets."
|
"secretDocs": "Check out the docs for more details about how to use secrets."
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"header": "Edit views",
|
||||||
|
"title": "Title",
|
||||||
|
"actions": "Actions",
|
||||||
|
"kind": "Kind",
|
||||||
|
"bucketConfigMode": "Bucket configuration mode",
|
||||||
|
"bucketConfig": "Bucket configuration",
|
||||||
|
"bucketConfigManual": "Manual",
|
||||||
|
"filter": "Filter",
|
||||||
|
"create": "Create view",
|
||||||
|
"createSuccess": "The view was created successfully.",
|
||||||
|
"titleRequired": "Please provide a title.",
|
||||||
|
"delete": "Delete this view",
|
||||||
|
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
|
||||||
|
"deleteSuccess": "The view was successfully deleted"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
@ -1049,7 +1065,8 @@
|
|||||||
"newProject": "New project",
|
"newProject": "New project",
|
||||||
"createProject": "Create project",
|
"createProject": "Create project",
|
||||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
|
||||||
|
"views": "Views"
|
||||||
},
|
},
|
||||||
"apiConfig": {
|
"apiConfig": {
|
||||||
"url": "Vikunja URL",
|
"url": "Vikunja URL",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type {IAbstract} from './IAbstract'
|
import type {IAbstract} from './IAbstract'
|
||||||
import type {IUser} from './IUser'
|
import type {IUser} from './IUser'
|
||||||
import type {ITask} from './ITask'
|
import type {ITask} from './ITask'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
export interface IBucket extends IAbstract {
|
export interface IBucket extends IAbstract {
|
||||||
id: number
|
id: number
|
||||||
@ -10,6 +11,7 @@ export interface IBucket extends IAbstract {
|
|||||||
tasks: ITask[]
|
tasks: ITask[]
|
||||||
position: number
|
position: number
|
||||||
count: number
|
count: number
|
||||||
|
projectViewId: IProjectView['id']
|
||||||
|
|
||||||
createdBy: IUser
|
createdBy: IUser
|
||||||
created: Date
|
created: Date
|
||||||
|
@ -2,6 +2,7 @@ import type {IAbstract} from './IAbstract'
|
|||||||
import type {ITask} from './ITask'
|
import type {ITask} from './ITask'
|
||||||
import type {IUser} from './IUser'
|
import type {IUser} from './IUser'
|
||||||
import type {ISubscription} from './ISubscription'
|
import type {ISubscription} from './ISubscription'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
|
|
||||||
export interface IProject extends IAbstract {
|
export interface IProject extends IAbstract {
|
||||||
@ -21,6 +22,7 @@ export interface IProject extends IAbstract {
|
|||||||
parentProjectId: number
|
parentProjectId: number
|
||||||
doneBucketId: number
|
doneBucketId: number
|
||||||
defaultBucketId: number
|
defaultBucketId: number
|
||||||
|
views: IProjectView[]
|
||||||
|
|
||||||
created: Date
|
created: Date
|
||||||
updated: Date
|
updated: Date
|
||||||
|
31
frontend/src/modelTypes/IProjectView.ts
Normal file
31
frontend/src/modelTypes/IProjectView.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type {IAbstract} from './IAbstract'
|
||||||
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
|
||||||
|
export const PROJECT_VIEW_KINDS = ['list', 'gantt', 'table', 'kanban']
|
||||||
|
export type ProjectViewKind = typeof PROJECT_VIEW_KINDS[number]
|
||||||
|
|
||||||
|
export const PROJECT_VIEW_BUCKET_CONFIGURATION_MODES = ['none', 'manual', 'filter']
|
||||||
|
export type ProjectViewBucketConfigurationMode = typeof PROJECT_VIEW_BUCKET_CONFIGURATION_MODES[number]
|
||||||
|
|
||||||
|
export interface IProjectViewBucketConfiguration {
|
||||||
|
title: string
|
||||||
|
filter: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProjectView extends IAbstract {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
projectId: IProject['id']
|
||||||
|
viewKind: ProjectViewKind
|
||||||
|
|
||||||
|
filter: string
|
||||||
|
position: number
|
||||||
|
|
||||||
|
bucketConfigurationMode: ProjectViewBucketConfigurationMode
|
||||||
|
bucketConfiguration: IProjectViewBucketConfiguration[]
|
||||||
|
defaultBucketId: number
|
||||||
|
doneBucketId: number
|
||||||
|
|
||||||
|
created: Date
|
||||||
|
updated: Date
|
||||||
|
}
|
8
frontend/src/modelTypes/ITaskPosition.ts
Normal file
8
frontend/src/modelTypes/ITaskPosition.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||||
|
|
||||||
|
export interface ITaskPosition extends IAbstract {
|
||||||
|
position: number
|
||||||
|
projectViewId: IProjectView['id']
|
||||||
|
taskId: number
|
||||||
|
}
|
@ -7,6 +7,7 @@ import type {IProject} from '@/modelTypes/IProject'
|
|||||||
import type {IUser} from '@/modelTypes/IUser'
|
import type {IUser} from '@/modelTypes/IUser'
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||||
|
import ProjectViewModel from '@/models/projectView'
|
||||||
|
|
||||||
export default class ProjectModel extends AbstractModel<IProject> implements IProject {
|
export default class ProjectModel extends AbstractModel<IProject> implements IProject {
|
||||||
id = 0
|
id = 0
|
||||||
@ -25,6 +26,7 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
|
|||||||
parentProjectId = 0
|
parentProjectId = 0
|
||||||
doneBucketId = 0
|
doneBucketId = 0
|
||||||
defaultBucketId = 0
|
defaultBucketId = 0
|
||||||
|
views = []
|
||||||
|
|
||||||
created: Date = null
|
created: Date = null
|
||||||
updated: Date = null
|
updated: Date = null
|
||||||
@ -48,6 +50,8 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
|
|||||||
this.subscription = new SubscriptionModel(this.subscription)
|
this.subscription = new SubscriptionModel(this.subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.views = this.views.map(v => new ProjectViewModel(v))
|
||||||
|
|
||||||
this.created = new Date(this.created)
|
this.created = new Date(this.created)
|
||||||
this.updated = new Date(this.updated)
|
this.updated = new Date(this.updated)
|
||||||
}
|
}
|
||||||
|
29
frontend/src/models/projectView.ts
Normal file
29
frontend/src/models/projectView.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type {IProjectView, ProjectViewBucketConfigurationMode, ProjectViewKind} from '@/modelTypes/IProjectView'
|
||||||
|
import AbstractModel from '@/models/abstractModel'
|
||||||
|
|
||||||
|
export default class ProjectViewModel extends AbstractModel<IProjectView> implements IProjectView {
|
||||||
|
id = 0
|
||||||
|
title = ''
|
||||||
|
projectId = 0
|
||||||
|
viewKind: ProjectViewKind = 'list'
|
||||||
|
|
||||||
|
filter = ''
|
||||||
|
position = 0
|
||||||
|
|
||||||
|
bucketConfiguration = []
|
||||||
|
bucketConfigurationMode: ProjectViewBucketConfigurationMode = 'manual'
|
||||||
|
defaultBucketId = 0
|
||||||
|
doneBucketId = 0
|
||||||
|
|
||||||
|
created: Date = new Date()
|
||||||
|
updated: Date = new Date()
|
||||||
|
|
||||||
|
constructor(data: Partial<IProjectView>) {
|
||||||
|
super()
|
||||||
|
this.assignData(data)
|
||||||
|
|
||||||
|
if (!this.bucketConfiguration) {
|
||||||
|
this.bucketConfiguration = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
frontend/src/models/taskPosition.ts
Normal file
13
frontend/src/models/taskPosition.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import AbstractModel from '@/models/abstractModel'
|
||||||
|
import type {ITaskPosition} from '@/modelTypes/ITaskPosition'
|
||||||
|
|
||||||
|
export default class TaskPositionModel extends AbstractModel<ITaskPosition> implements ITaskPosition {
|
||||||
|
position = 0
|
||||||
|
projectViewId = 0
|
||||||
|
taskId = 0
|
||||||
|
|
||||||
|
constructor(data: Partial<ITaskPosition>) {
|
||||||
|
super()
|
||||||
|
this.assignData(data)
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,11 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import type { RouteLocation } from 'vue-router'
|
import type { RouteLocation } from 'vue-router'
|
||||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||||
|
|
||||||
import {saveProjectView, getProjectView} from '@/helpers/projectView'
|
import {saveProjectView, getProjectViewId} from '@/helpers/projectView'
|
||||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||||
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
|
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
|
||||||
import {setTitle} from '@/helpers/setTitle'
|
|
||||||
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
|
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
|
||||||
|
|
||||||
import {useProjectStore} from '@/stores/projects'
|
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
|
|
||||||
@ -33,15 +31,8 @@ const NewLabelComponent = () => import('@/views/labels/NewLabel.vue')
|
|||||||
// Migration
|
// Migration
|
||||||
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
|
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
|
||||||
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
|
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
|
||||||
// Project Views
|
// Project View
|
||||||
const ProjectList = () => import('@/views/project/ProjectList.vue')
|
import ProjectView from '@/views/project/ProjectView.vue'
|
||||||
const ProjectGantt = () => import('@/views/project/ProjectGantt.vue')
|
|
||||||
const ProjectTable = () => import('@/views/project/ProjectTable.vue')
|
|
||||||
// If we load the component async, using it as a backdrop view will not work. Instead, everything explodes
|
|
||||||
// with an error from the core saying "Cannot read properties of undefined (reading 'parentNode')"
|
|
||||||
// Of course, with no clear indicator of where the problem comes from.
|
|
||||||
// const ProjectKanban = () => import('@/views/project/ProjectKanban.vue')
|
|
||||||
import ProjectKanban from '@/views/project/ProjectKanban.vue'
|
|
||||||
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
|
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
|
||||||
|
|
||||||
// Project Settings
|
// Project Settings
|
||||||
@ -53,6 +44,7 @@ const ProjectSettingShare = () => import('@/views/project/settings/share.vue')
|
|||||||
const ProjectSettingWebhooks = () => import('@/views/project/settings/webhooks.vue')
|
const ProjectSettingWebhooks = () => import('@/views/project/settings/webhooks.vue')
|
||||||
const ProjectSettingDelete = () => import('@/views/project/settings/delete.vue')
|
const ProjectSettingDelete = () => import('@/views/project/settings/delete.vue')
|
||||||
const ProjectSettingArchive = () => import('@/views/project/settings/archive.vue')
|
const ProjectSettingArchive = () => import('@/views/project/settings/archive.vue')
|
||||||
|
const ProjectSettingViews = () => import('@/views/project/settings/views.vue')
|
||||||
|
|
||||||
// Saved Filters
|
// Saved Filters
|
||||||
const FilterNew = () => import('@/views/filters/FilterNew.vue')
|
const FilterNew = () => import('@/views/filters/FilterNew.vue')
|
||||||
@ -315,6 +307,15 @@ const router = createRouter({
|
|||||||
showAsModal: true,
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/projects/:projectId/settings/views',
|
||||||
|
name: 'project.settings.views',
|
||||||
|
component: ProjectSettingViews,
|
||||||
|
meta: {
|
||||||
|
showAsModal: true,
|
||||||
|
},
|
||||||
|
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/projects/:projectId/settings/edit',
|
path: '/projects/:projectId/settings/edit',
|
||||||
name: 'filter.settings.edit',
|
name: 'filter.settings.edit',
|
||||||
@ -346,55 +347,31 @@ const router = createRouter({
|
|||||||
path: '/projects/:projectId',
|
path: '/projects/:projectId',
|
||||||
name: 'project.index',
|
name: 'project.index',
|
||||||
redirect(to) {
|
redirect(to) {
|
||||||
// Redirect the user to list view by default
|
const viewId = getProjectViewId(Number(to.params.projectId as string))
|
||||||
const savedProjectView = getProjectView(Number(to.params.projectId as string))
|
console.log(viewId)
|
||||||
|
|
||||||
if (savedProjectView) {
|
if (viewId) {
|
||||||
console.log('Replaced list view with', savedProjectView)
|
console.debug('Replaced list view with', viewId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: savedProjectView || 'project.list',
|
name: 'project.view',
|
||||||
params: {projectId: to.params.projectId},
|
params: {
|
||||||
|
projectId: parseInt(to.params.projectId as string),
|
||||||
|
viewId: viewId ?? 0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/projects/:projectId/list',
|
path: '/projects/:projectId/:viewId',
|
||||||
name: 'project.list',
|
name: 'project.view',
|
||||||
component: ProjectList,
|
component: ProjectView,
|
||||||
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
|
beforeEnter: (to) => saveProjectView(parseInt(to.params.projectId as string), parseInt(to.params.viewId as string)),
|
||||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
props: route => ({
|
||||||
},
|
projectId: parseInt(route.params.projectId as string),
|
||||||
{
|
viewId: route.params.viewId ? parseInt(route.params.viewId as string): undefined,
|
||||||
path: '/projects/:projectId/gantt',
|
}),
|
||||||
name: 'project.gantt',
|
|
||||||
component: ProjectGantt,
|
|
||||||
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
|
|
||||||
// FIXME: test if `useRoute` would be the same. If it would use it instead.
|
|
||||||
props: route => ({route}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/projects/:projectId/table',
|
|
||||||
name: 'project.table',
|
|
||||||
component: ProjectTable,
|
|
||||||
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
|
|
||||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/projects/:projectId/kanban',
|
|
||||||
name: 'project.kanban',
|
|
||||||
component: ProjectKanban,
|
|
||||||
beforeEnter: (to) => {
|
|
||||||
saveProjectView(to.params.projectId, to.name)
|
|
||||||
// Properly set the page title when a task popup is closed
|
|
||||||
const projectStore = useProjectStore()
|
|
||||||
const projectFromStore = projectStore.projects[Number(to.params.projectId)]
|
|
||||||
if(projectFromStore) {
|
|
||||||
setTitle(projectFromStore.title)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/teams',
|
path: '/teams',
|
||||||
|
@ -6,10 +6,10 @@ import type { IBucket } from '@/modelTypes/IBucket'
|
|||||||
export default class BucketService extends AbstractService<IBucket> {
|
export default class BucketService extends AbstractService<IBucket> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
getAll: '/projects/{projectId}/buckets',
|
getAll: '/projects/{projectId}/views/{projectViewId}/buckets',
|
||||||
create: '/projects/{projectId}/buckets',
|
create: '/projects/{projectId}/views/{projectViewId}/buckets',
|
||||||
update: '/projects/{projectId}/buckets/{id}',
|
update: '/projects/{projectId}/views/{projectViewId}/buckets/{id}',
|
||||||
delete: '/projects/{projectId}/buckets/{id}',
|
delete: '/projects/{projectId}/views/{projectViewId}/buckets/{id}',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
20
frontend/src/services/projectViews.ts
Normal file
20
frontend/src/services/projectViews.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import AbstractService from '@/services/abstractService'
|
||||||
|
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||||
|
import ProjectViewModel from '@/models/projectView'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
|
export default class ProjectViewService extends AbstractService<IProjectView> {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
get: '/projects/{projectId}/views/{id}',
|
||||||
|
getAll: '/projects/{projectId}/views',
|
||||||
|
create: '/projects/{projectId}/views',
|
||||||
|
update: '/projects/{projectId}/views/{id}',
|
||||||
|
delete: '/projects/{projectId}/views/{id}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
modelFactory(data: Partial<IAbstract>): ProjectViewModel {
|
||||||
|
return new ProjectViewModel(data)
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import AbstractService from '@/services/abstractService'
|
|||||||
import TaskModel from '@/models/task'
|
import TaskModel from '@/models/task'
|
||||||
|
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
import BucketModel from '@/models/bucket'
|
||||||
|
|
||||||
export interface TaskFilterParams {
|
export interface TaskFilterParams {
|
||||||
sort_by: ('start_date' | 'end_date' | 'due_date' | 'done' | 'id' | 'position' | 'kanban_position')[],
|
sort_by: ('start_date' | 'end_date' | 'due_date' | 'done' | 'id' | 'position' | 'kanban_position')[],
|
||||||
@ -27,11 +28,15 @@ export function getDefaultTaskFilterParams(): TaskFilterParams {
|
|||||||
export default class TaskCollectionService extends AbstractService<ITask> {
|
export default class TaskCollectionService extends AbstractService<ITask> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
getAll: '/projects/{projectId}/tasks',
|
getAll: '/projects/{projectId}/views/{viewId}/tasks',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
modelFactory(data) {
|
modelFactory(data) {
|
||||||
|
// FIXME: There must be a better way for this…
|
||||||
|
if (typeof data.project_view_id !== 'undefined') {
|
||||||
|
return new BucketModel(data)
|
||||||
|
}
|
||||||
return new TaskModel(data)
|
return new TaskModel(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
15
frontend/src/services/taskPosition.ts
Normal file
15
frontend/src/services/taskPosition.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import AbstractService from '@/services/abstractService'
|
||||||
|
import type {ITaskPosition} from '@/modelTypes/ITaskPosition'
|
||||||
|
import TaskPositionModel from '@/models/taskPosition'
|
||||||
|
|
||||||
|
export default class TaskPositionService extends AbstractService<ITaskPosition> {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
update: '/tasks/{taskId}/position',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
modelFactory(data: Partial<ITaskPosition>) {
|
||||||
|
return new TaskPositionModel(data)
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,6 @@ import {acceptHMRUpdate, defineStore} from 'pinia'
|
|||||||
import {klona} from 'klona/lite'
|
import {klona} from 'klona/lite'
|
||||||
|
|
||||||
import {findById, findIndexById} from '@/helpers/utils'
|
import {findById, findIndexById} from '@/helpers/utils'
|
||||||
import {i18n} from '@/i18n'
|
|
||||||
import {success} from '@/message'
|
|
||||||
|
|
||||||
import BucketService from '@/services/bucket'
|
import BucketService from '@/services/bucket'
|
||||||
import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection'
|
import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection'
|
||||||
@ -15,6 +13,7 @@ import type {ITask} from '@/modelTypes/ITask'
|
|||||||
import type {IProject} from '@/modelTypes/IProject'
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
import type {IBucket} from '@/modelTypes/IBucket'
|
import type {IBucket} from '@/modelTypes/IBucket'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
const TASKS_PER_BUCKET = 25
|
const TASKS_PER_BUCKET = 25
|
||||||
|
|
||||||
@ -176,10 +175,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||||||
buckets.value[bucketIndex] = newBucket
|
buckets.value[bucketIndex] = newBucket
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTasksToBucket({tasks, bucketId}: {
|
function addTasksToBucket(tasks: ITask[], bucketId: IBucket['id']) {
|
||||||
tasks: ITask[];
|
|
||||||
bucketId: IBucket['id'];
|
|
||||||
}) {
|
|
||||||
const bucketIndex = findIndexById(buckets.value, bucketId)
|
const bucketIndex = findIndexById(buckets.value, bucketId)
|
||||||
const oldBucket = buckets.value[bucketIndex]
|
const oldBucket = buckets.value[bucketIndex]
|
||||||
const newBucket = {
|
const newBucket = {
|
||||||
@ -225,15 +221,15 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||||||
allTasksLoadedForBucket.value[bucketId] = true
|
allTasksLoadedForBucket.value[bucketId] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBucketsForProject({projectId, params}: { projectId: IProject['id'], params }) {
|
async function loadBucketsForProject(projectId: IProject['id'], viewId: IProjectView['id'], params) {
|
||||||
const cancel = setModuleLoading(setIsLoading)
|
const cancel = setModuleLoading(setIsLoading)
|
||||||
|
|
||||||
// Clear everything to prevent having old buckets in the project if loading the buckets from this project takes a few moments
|
// Clear everything to prevent having old buckets in the project if loading the buckets from this project takes a few moments
|
||||||
setBuckets([])
|
setBuckets([])
|
||||||
|
|
||||||
const bucketService = new BucketService()
|
const taskCollectionService = new TaskCollectionService()
|
||||||
try {
|
try {
|
||||||
const newBuckets = await bucketService.getAll({projectId}, {
|
const newBuckets = await taskCollectionService.getAll({projectId, viewId}, {
|
||||||
...params,
|
...params,
|
||||||
per_page: TASKS_PER_BUCKET,
|
per_page: TASKS_PER_BUCKET,
|
||||||
})
|
})
|
||||||
@ -247,6 +243,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||||||
|
|
||||||
async function loadNextTasksForBucket(
|
async function loadNextTasksForBucket(
|
||||||
projectId: IProject['id'],
|
projectId: IProject['id'],
|
||||||
|
viewId: IProjectView['id'],
|
||||||
ps: TaskFilterParams,
|
ps: TaskFilterParams,
|
||||||
bucketId: IBucket['id'],
|
bucketId: IBucket['id'],
|
||||||
) {
|
) {
|
||||||
@ -267,7 +264,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||||||
|
|
||||||
const params: TaskFilterParams = JSON.parse(JSON.stringify(ps))
|
const params: TaskFilterParams = JSON.parse(JSON.stringify(ps))
|
||||||
|
|
||||||
params.sort_by = ['kanban_position']
|
params.sort_by = ['position']
|
||||||
params.order_by = ['asc']
|
params.order_by = ['asc']
|
||||||
params.filter = `${params.filter === '' ? '' : params.filter + ' && '}bucket_id = ${bucketId}`
|
params.filter = `${params.filter === '' ? '' : params.filter + ' && '}bucket_id = ${bucketId}`
|
||||||
params.filter_timezone = authStore.settings.timezone
|
params.filter_timezone = authStore.settings.timezone
|
||||||
@ -275,8 +272,8 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||||||
|
|
||||||
const taskService = new TaskCollectionService()
|
const taskService = new TaskCollectionService()
|
||||||
try {
|
try {
|
||||||
const tasks = await taskService.getAll({projectId}, params, page)
|
const tasks = await taskService.getAll({projectId, viewId}, params, page)
|
||||||
addTasksToBucket({tasks, bucketId: bucketId})
|
addTasksToBucket(tasks, bucketId)
|
||||||
setTasksLoadedForBucketPage({bucketId, page})
|
setTasksLoadedForBucketPage({bucketId, page})
|
||||||
if (taskService.totalPages <= page) {
|
if (taskService.totalPages <= page) {
|
||||||
setAllTasksLoadedForBucket(bucketId)
|
setAllTasksLoadedForBucket(bucketId)
|
||||||
@ -309,7 +306,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||||||
const response = await bucketService.delete(bucket)
|
const response = await bucketService.delete(bucket)
|
||||||
removeBucket(bucket)
|
removeBucket(bucket)
|
||||||
// We reload all buckets because tasks are being moved from the deleted bucket
|
// We reload all buckets because tasks are being moved from the deleted bucket
|
||||||
loadBucketsForProject({projectId: bucket.projectId, params})
|
loadBucketsForProject(bucket.projectId, bucket.projectViewId, params)
|
||||||
return response
|
return response
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
@ -344,18 +341,6 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateBucketTitle({id, title}: { id: IBucket['id'], title: IBucket['title'] }) {
|
|
||||||
const bucket = findById(buckets.value, id)
|
|
||||||
|
|
||||||
if (bucket?.title === title) {
|
|
||||||
// bucket title has not changed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateBucket({id, title})
|
|
||||||
success({message: i18n.global.t('project.kanban.bucketTitleSavedSuccess')})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buckets,
|
buckets,
|
||||||
isLoading: readonly(isLoading),
|
isLoading: readonly(isLoading),
|
||||||
@ -374,7 +359,6 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||||||
createBucket,
|
createBucket,
|
||||||
deleteBucket,
|
deleteBucket,
|
||||||
updateBucket,
|
updateBucket,
|
||||||
updateBucketTitle,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import ProjectModel from '@/models/project'
|
|||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
|
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
||||||
|
|
||||||
@ -211,6 +212,26 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setProjectView(view: IProjectView) {
|
||||||
|
const viewPos = projects.value[view.projectId].views.findIndex(v => v.id === view.id)
|
||||||
|
if (viewPos !== -1) {
|
||||||
|
projects.value[view.projectId].views[viewPos] = view
|
||||||
|
setProject(projects.value[view.projectId])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projects.value[view.projectId].views.push(view)
|
||||||
|
|
||||||
|
setProject(projects.value[view.projectId])
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeProjectView(projectId: IProject['id'], viewId: IProjectView['id']) {
|
||||||
|
const viewPos = projects.value[projectId].views.findIndex(v => v.id === viewId)
|
||||||
|
if (viewPos !== -1) {
|
||||||
|
projects.value[projectId].views.splice(viewPos, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading: readonly(isLoading),
|
isLoading: readonly(isLoading),
|
||||||
projects: readonly(projects),
|
projects: readonly(projects),
|
||||||
@ -235,6 +256,8 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
updateProject,
|
updateProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
getAncestors,
|
getAncestors,
|
||||||
|
setProjectView,
|
||||||
|
removeProjectView,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ import {useKanbanStore} from '@/stores/kanban'
|
|||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import ProjectUserService from '@/services/projectUsers'
|
import ProjectUserService from '@/services/projectUsers'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection'
|
import {type TaskFilterParams} from '@/services/taskCollection'
|
||||||
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
||||||
|
|
||||||
interface MatchedAssignee extends IUser {
|
interface MatchedAssignee extends IUser {
|
||||||
@ -124,21 +124,23 @@ export const useTaskStore = defineStore('task', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTasks(params: TaskFilterParams, projectId: IProject['id'] | null = null) {
|
async function loadTasks(
|
||||||
|
params: TaskFilterParams,
|
||||||
|
projectId: IProject['id'] | null = null,
|
||||||
|
) {
|
||||||
|
|
||||||
if (!params.filter_timezone || params.filter_timezone === '') {
|
if (!params.filter_timezone || params.filter_timezone === '') {
|
||||||
params.filter_timezone = authStore.settings.timezone
|
params.filter_timezone = authStore.settings.timezone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (projectId !== null) {
|
||||||
|
params.filter = 'project = '+projectId+' && (' + params.filter +')'
|
||||||
|
}
|
||||||
|
|
||||||
const cancel = setModuleLoading(setIsLoading)
|
const cancel = setModuleLoading(setIsLoading)
|
||||||
try {
|
try {
|
||||||
if (projectId === null) {
|
const taskService = new TaskService()
|
||||||
const taskService = new TaskService()
|
tasks.value = await taskService.getAll({}, params)
|
||||||
tasks.value = await taskService.getAll({}, params)
|
|
||||||
} else {
|
|
||||||
const taskCollectionService = new TaskCollectionService()
|
|
||||||
tasks.value = await taskCollectionService.getAll({projectId}, params)
|
|
||||||
}
|
|
||||||
baseStore.setHasTasks(tasks.value.length > 0)
|
baseStore.setHasTasks(tasks.value.length > 0)
|
||||||
return tasks.value
|
return tasks.value
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
export const PROJECT_VIEWS = {
|
|
||||||
LIST: 'list',
|
|
||||||
GANTT: 'gantt',
|
|
||||||
TABLE: 'table',
|
|
||||||
KANBAN: 'kanban',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export type ProjectView = typeof PROJECT_VIEWS[keyof typeof PROJECT_VIEWS]
|
|
80
frontend/src/views/project/ProjectView.vue
Normal file
80
frontend/src/views/project/ProjectView.vue
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {computed, watch} from 'vue'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
|
|
||||||
|
import ProjectList from '@/components/project/views/ProjectList.vue'
|
||||||
|
import ProjectGantt from '@/components/project/views/ProjectGantt.vue'
|
||||||
|
import ProjectTable from '@/components/project/views/ProjectTable.vue'
|
||||||
|
import ProjectKanban from '@/components/project/views/ProjectKanban.vue'
|
||||||
|
|
||||||
|
const {
|
||||||
|
projectId,
|
||||||
|
viewId,
|
||||||
|
} = defineProps<{
|
||||||
|
projectId: number,
|
||||||
|
viewId: number,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
|
||||||
|
const currentView = computed(() => {
|
||||||
|
const project = projectStore.projects[projectId]
|
||||||
|
|
||||||
|
return project?.views.find(v => v.id === viewId)
|
||||||
|
})
|
||||||
|
|
||||||
|
function redirectToFirstViewIfNecessary() {
|
||||||
|
if (viewId === 0) {
|
||||||
|
// Ideally, we would do that in the router redirect, but the projects (and therefore, the views)
|
||||||
|
// are not always loaded then.
|
||||||
|
const firstViewId = projectStore.projects[projectId]?.views[0].id
|
||||||
|
if (firstViewId) {
|
||||||
|
router.replace({
|
||||||
|
name: 'project.view',
|
||||||
|
params: {
|
||||||
|
projectId,
|
||||||
|
viewId: firstViewId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => viewId,
|
||||||
|
redirectToFirstViewIfNecessary,
|
||||||
|
{immediate: true},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => projectStore.projects[projectId],
|
||||||
|
redirectToFirstViewIfNecessary,
|
||||||
|
)
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ProjectList
|
||||||
|
v-if="currentView?.viewKind === 'list'"
|
||||||
|
:project-id="projectId"
|
||||||
|
:view-id
|
||||||
|
/>
|
||||||
|
<ProjectGantt
|
||||||
|
v-if="currentView?.viewKind === 'gantt'"
|
||||||
|
:route
|
||||||
|
:view-id
|
||||||
|
/>
|
||||||
|
<ProjectTable
|
||||||
|
v-if="currentView?.viewKind === 'table'"
|
||||||
|
:project-id="projectId"
|
||||||
|
:view-id
|
||||||
|
/>
|
||||||
|
<ProjectKanban
|
||||||
|
v-if="currentView?.viewKind === 'kanban'"
|
||||||
|
:project-id="projectId"
|
||||||
|
:view-id
|
||||||
|
/>
|
||||||
|
</template>
|
@ -12,10 +12,12 @@ import type {TaskFilterParams} from '@/services/taskCollection'
|
|||||||
|
|
||||||
import type {DateISO} from '@/types/DateISO'
|
import type {DateISO} from '@/types/DateISO'
|
||||||
import type {DateKebab} from '@/types/DateKebab'
|
import type {DateKebab} from '@/types/DateKebab'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
// convenient internal filter object
|
// convenient internal filter object
|
||||||
export interface GanttFilters {
|
export interface GanttFilters {
|
||||||
projectId: IProject['id']
|
projectId: IProject['id']
|
||||||
|
viewId: IProjectView['id'],
|
||||||
dateFrom: DateISO
|
dateFrom: DateISO
|
||||||
dateTo: DateISO
|
dateTo: DateISO
|
||||||
showTasksWithoutDates: boolean
|
showTasksWithoutDates: boolean
|
||||||
@ -41,6 +43,7 @@ function ganttRouteToFilters(route: Partial<RouteLocationNormalized>): GanttFilt
|
|||||||
const ganttRoute = route
|
const ganttRoute = route
|
||||||
return {
|
return {
|
||||||
projectId: Number(ganttRoute.params?.projectId),
|
projectId: Number(ganttRoute.params?.projectId),
|
||||||
|
viewId: Number(ganttRoute.params?.viewId),
|
||||||
dateFrom: parseDateProp(ganttRoute.query?.dateFrom as DateKebab) || getDefaultDateFrom(),
|
dateFrom: parseDateProp(ganttRoute.query?.dateFrom as DateKebab) || getDefaultDateFrom(),
|
||||||
dateTo: parseDateProp(ganttRoute.query?.dateTo as DateKebab) || getDefaultDateTo(),
|
dateTo: parseDateProp(ganttRoute.query?.dateTo as DateKebab) || getDefaultDateTo(),
|
||||||
showTasksWithoutDates: parseBooleanProp(ganttRoute.query?.showTasksWithoutDates as string) || DEFAULT_SHOW_TASKS_WITHOUT_DATES,
|
showTasksWithoutDates: parseBooleanProp(ganttRoute.query?.showTasksWithoutDates as string) || DEFAULT_SHOW_TASKS_WITHOUT_DATES,
|
||||||
@ -69,8 +72,11 @@ function ganttFiltersToRoute(filters: GanttFilters): RouteLocationRaw {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'project.gantt',
|
name: 'project.view',
|
||||||
params: {projectId: filters.projectId},
|
params: {
|
||||||
|
projectId: filters.projectId,
|
||||||
|
viewId: filters.viewId,
|
||||||
|
},
|
||||||
query,
|
query,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,7 +94,7 @@ export type UseGanttFiltersReturn =
|
|||||||
ReturnType<typeof useRouteFilters<GanttFilters>> &
|
ReturnType<typeof useRouteFilters<GanttFilters>> &
|
||||||
ReturnType<typeof useGanttTaskList<GanttFilters>>
|
ReturnType<typeof useGanttTaskList<GanttFilters>>
|
||||||
|
|
||||||
export function useGanttFilters(route: Ref<RouteLocationNormalized>): UseGanttFiltersReturn {
|
export function useGanttFilters(route: Ref<RouteLocationNormalized>, viewId: IProjectView['id']): UseGanttFiltersReturn {
|
||||||
const {
|
const {
|
||||||
filters,
|
filters,
|
||||||
hasDefaultFilters,
|
hasDefaultFilters,
|
||||||
@ -98,7 +104,7 @@ export function useGanttFilters(route: Ref<RouteLocationNormalized>): UseGanttFi
|
|||||||
ganttGetDefaultFilters,
|
ganttGetDefaultFilters,
|
||||||
ganttRouteToFilters,
|
ganttRouteToFilters,
|
||||||
ganttFiltersToRoute,
|
ganttFiltersToRoute,
|
||||||
['project.gantt'],
|
['project.view'],
|
||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -108,7 +114,7 @@ export function useGanttFilters(route: Ref<RouteLocationNormalized>): UseGanttFi
|
|||||||
isLoading,
|
isLoading,
|
||||||
addTask,
|
addTask,
|
||||||
updateTask,
|
updateTask,
|
||||||
} = useGanttTaskList<GanttFilters>(filters, ganttFiltersToApiParams)
|
} = useGanttTaskList<GanttFilters>(filters, ganttFiltersToApiParams, viewId)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filters,
|
filters,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {computed, ref, shallowReactive, watch, type Ref} from 'vue'
|
import {computed, ref, type Ref, shallowReactive, watch} from 'vue'
|
||||||
import {klona} from 'klona/lite'
|
import {klona} from 'klona/lite'
|
||||||
|
|
||||||
import type {Filters} from '@/composables/useRouteFilters'
|
import type {Filters} from '@/composables/useRouteFilters'
|
||||||
@ -10,16 +10,15 @@ import TaskService from '@/services/task'
|
|||||||
import TaskModel from '@/models/task'
|
import TaskModel from '@/models/task'
|
||||||
import {error, success} from '@/message'
|
import {error, success} from '@/message'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
|
||||||
// FIXME: unify with general `useTaskList`
|
// FIXME: unify with general `useTaskList`
|
||||||
export function useGanttTaskList<F extends Filters>(
|
export function useGanttTaskList<F extends Filters>(
|
||||||
filters: Ref<F>,
|
filters: Ref<F>,
|
||||||
filterToApiParams: (filters: F) => TaskFilterParams,
|
filterToApiParams: (filters: F) => TaskFilterParams,
|
||||||
options: {
|
viewId: IProjectView['id'],
|
||||||
loadAll?: boolean,
|
loadAll: boolean = true,
|
||||||
} = {
|
) {
|
||||||
loadAll: true,
|
|
||||||
}) {
|
|
||||||
const taskCollectionService = shallowReactive(new TaskCollectionService())
|
const taskCollectionService = shallowReactive(new TaskCollectionService())
|
||||||
const taskService = shallowReactive(new TaskService())
|
const taskService = shallowReactive(new TaskService())
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@ -30,12 +29,12 @@ export function useGanttTaskList<F extends Filters>(
|
|||||||
|
|
||||||
async function fetchTasks(params: TaskFilterParams, page = 1): Promise<ITask[]> {
|
async function fetchTasks(params: TaskFilterParams, page = 1): Promise<ITask[]> {
|
||||||
|
|
||||||
if(params.filter_timezone === '') {
|
if (params.filter_timezone === '') {
|
||||||
params.filter_timezone = authStore.settings.timezone
|
params.filter_timezone = authStore.settings.timezone
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = await taskCollectionService.getAll({projectId: filters.value.projectId}, params, page) as ITask[]
|
const tasks = await taskCollectionService.getAll({projectId: filters.value.projectId, viewId}, params, page) as ITask[]
|
||||||
if (options.loadAll && page < taskCollectionService.totalPages) {
|
if (loadAll && page < taskCollectionService.totalPages) {
|
||||||
const nextTasks = await fetchTasks(params, page + 1)
|
const nextTasks = await fetchTasks(params, page + 1)
|
||||||
return tasks.concat(nextTasks)
|
return tasks.concat(nextTasks)
|
||||||
}
|
}
|
||||||
|
178
frontend/src/views/project/settings/views.vue
Normal file
178
frontend/src/views/project/settings/views.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||||
|
import {computed, ref} from 'vue'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
import ProjectViewModel from '@/models/projectView'
|
||||||
|
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||||
|
import ViewEditForm from '@/components/project/views/viewEditForm.vue'
|
||||||
|
import ProjectViewService from '@/services/projectViews'
|
||||||
|
import XButton from '@/components/input/button.vue'
|
||||||
|
import {error, success} from '@/message'
|
||||||
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
|
const {
|
||||||
|
projectId,
|
||||||
|
} = defineProps<{
|
||||||
|
projectId: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
const {t} = useI18n()
|
||||||
|
|
||||||
|
const views = computed(() => projectStore.projects[projectId]?.views)
|
||||||
|
const showCreateForm = ref(false)
|
||||||
|
|
||||||
|
const projectViewService = ref(new ProjectViewService())
|
||||||
|
const newView = ref<IProjectView>(new ProjectViewModel({}))
|
||||||
|
const viewIdToDelete = ref<number | null>(null)
|
||||||
|
const showDeleteModal = ref(false)
|
||||||
|
const viewToEdit = ref<IProjectView | null>(null)
|
||||||
|
|
||||||
|
async function createView() {
|
||||||
|
if (!showCreateForm.value) {
|
||||||
|
showCreateForm.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newView.value.title === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
newView.value.bucketConfigurationMode = newView.value.viewKind === 'kanban'
|
||||||
|
? newView.value.bucketConfigurationMode
|
||||||
|
: 'none'
|
||||||
|
newView.value.projectId = projectId
|
||||||
|
|
||||||
|
const result: IProjectView = await projectViewService.value.create(newView.value)
|
||||||
|
success({message: t('project.views.createSuccess')})
|
||||||
|
showCreateForm.value = false
|
||||||
|
projectStore.setProjectView(result)
|
||||||
|
newView.value = new ProjectViewModel({})
|
||||||
|
} catch (e) {
|
||||||
|
error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteView() {
|
||||||
|
if (!viewIdToDelete.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectViewService.value.delete(new ProjectViewModel({
|
||||||
|
id: viewIdToDelete.value,
|
||||||
|
projectId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
projectStore.removeProjectView(projectId, viewIdToDelete.value)
|
||||||
|
|
||||||
|
showDeleteModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveView() {
|
||||||
|
if (viewToEdit.value?.viewKind !== 'kanban') {
|
||||||
|
viewToEdit.value.bucketConfigurationMode = 'none'
|
||||||
|
}
|
||||||
|
const result = await projectViewService.value.update(viewToEdit.value)
|
||||||
|
projectStore.setProjectView(result)
|
||||||
|
viewToEdit.value = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CreateEdit
|
||||||
|
:title="$t('project.views.header')"
|
||||||
|
:primary-label="$t('misc.save')"
|
||||||
|
>
|
||||||
|
<ViewEditForm
|
||||||
|
v-if="showCreateForm"
|
||||||
|
v-model="newView"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<div class="is-flex is-justify-content-end">
|
||||||
|
<XButton
|
||||||
|
:loading="projectViewService.loading"
|
||||||
|
@click="createView"
|
||||||
|
>
|
||||||
|
{{ $t('project.views.create') }}
|
||||||
|
</XButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table
|
||||||
|
v-if="views?.length > 0"
|
||||||
|
class="table has-actions is-striped is-hoverable is-fullwidth"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('project.views.title') }}</th>
|
||||||
|
<th>{{ $t('project.views.kind') }}</th>
|
||||||
|
<th class="has-text-right">
|
||||||
|
{{ $t('project.views.actions') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="v in views"
|
||||||
|
:key="v.id"
|
||||||
|
>
|
||||||
|
<template v-if="viewToEdit !== null && viewToEdit.id === v.id">
|
||||||
|
<td colspan="3">
|
||||||
|
<ViewEditForm
|
||||||
|
v-model="viewToEdit"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
<div class="is-flex is-justify-content-end">
|
||||||
|
<XButton
|
||||||
|
variant="tertiary"
|
||||||
|
class="mr-2"
|
||||||
|
@click="viewToEdit = null"
|
||||||
|
>
|
||||||
|
{{ $t('misc.cancel') }}
|
||||||
|
</XButton>
|
||||||
|
<XButton
|
||||||
|
:loading="projectViewService.loading"
|
||||||
|
@click="saveView"
|
||||||
|
>
|
||||||
|
{{ $t('misc.save') }}
|
||||||
|
</XButton>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<td>{{ v.title }}</td>
|
||||||
|
<td>{{ v.viewKind }}</td>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<XButton
|
||||||
|
class="is-danger mr-2"
|
||||||
|
icon="trash-alt"
|
||||||
|
@click="() => {
|
||||||
|
viewIdToDelete = v.id
|
||||||
|
showDeleteModal = true
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<XButton
|
||||||
|
icon="pen"
|
||||||
|
@click="viewToEdit = {...v}"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CreateEdit>
|
||||||
|
|
||||||
|
<modal
|
||||||
|
:enabled="showDeleteModal"
|
||||||
|
@close="showDeleteModal = false"
|
||||||
|
@submit="deleteView"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span>{{ $t('project.views.delete') }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #text>
|
||||||
|
<p>{{ $t('project.views.deleteText') }}</p>
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
</template>
|
@ -49,7 +49,6 @@ import {useI18n} from 'vue-i18n'
|
|||||||
import {useTitle} from '@vueuse/core'
|
import {useTitle} from '@vueuse/core'
|
||||||
|
|
||||||
import Message from '@/components/misc/message.vue'
|
import Message from '@/components/misc/message.vue'
|
||||||
import {PROJECT_VIEWS, type ProjectView} from '@/types/ProjectView'
|
|
||||||
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
|
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
@ -96,10 +95,6 @@ function useAuth() {
|
|||||||
: true
|
: true
|
||||||
baseStore.setLogoVisible(logoVisible)
|
baseStore.setLogoVisible(logoVisible)
|
||||||
|
|
||||||
const view = route.query.view && Object.values(PROJECT_VIEWS).includes(route.query.view as ProjectView)
|
|
||||||
? route.query.view
|
|
||||||
: 'list'
|
|
||||||
|
|
||||||
const hash = LINK_SHARE_HASH_PREFIX + route.params.share
|
const hash = LINK_SHARE_HASH_PREFIX + route.params.share
|
||||||
|
|
||||||
const last = getLastVisitedRoute()
|
const last = getLastVisitedRoute()
|
||||||
@ -111,8 +106,10 @@ function useAuth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return router.push({
|
return router.push({
|
||||||
name: `project.${view}`,
|
name: 'project.index',
|
||||||
params: {projectId},
|
params: {
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
hash,
|
hash,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
- id: 1
|
- id: 1
|
||||||
title: testbucket1
|
title: testbucket1
|
||||||
project_id: 1
|
project_view_id: 4
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
limit: 9999999 # This bucket has a limit we will never exceed in the tests to make sure the logic allows for buckets with limits
|
limit: 9999999 # This bucket has a limit we will never exceed in the tests to make sure the logic allows for buckets with limits
|
||||||
position: 1
|
position: 1
|
||||||
@ -8,7 +8,7 @@
|
|||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 2
|
- id: 2
|
||||||
title: testbucket2
|
title: testbucket2
|
||||||
project_id: 1
|
project_view_id: 4
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
limit: 3
|
limit: 3
|
||||||
position: 2
|
position: 2
|
||||||
@ -16,14 +16,14 @@
|
|||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 3
|
- id: 3
|
||||||
title: testbucket3
|
title: testbucket3
|
||||||
project_id: 1
|
project_view_id: 4
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
position: 3
|
position: 3
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 4
|
- id: 4
|
||||||
title: testbucket4 - other project
|
title: testbucket4 - other project
|
||||||
project_id: 2
|
project_view_id: 8
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
position: 1
|
position: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
@ -31,221 +31,221 @@
|
|||||||
# The following are not or only partly owned by user 1
|
# The following are not or only partly owned by user 1
|
||||||
- id: 5
|
- id: 5
|
||||||
title: testbucket5
|
title: testbucket5
|
||||||
project_id: 20
|
project_view_id: 80
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 6
|
- id: 6
|
||||||
title: testbucket6
|
title: testbucket6
|
||||||
project_id: 6
|
project_view_id: 24
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
position: 1
|
position: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 7
|
- id: 7
|
||||||
title: testbucket7
|
title: testbucket7
|
||||||
project_id: 7
|
project_view_id: 28
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 8
|
- id: 8
|
||||||
title: testbucket8
|
title: testbucket8
|
||||||
project_id: 8
|
project_view_id: 32
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 9
|
- id: 9
|
||||||
title: testbucket9
|
title: testbucket9
|
||||||
project_id: 9
|
project_view_id: 36
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 10
|
- id: 10
|
||||||
title: testbucket10
|
title: testbucket10
|
||||||
project_id: 10
|
project_view_id: 40
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 11
|
- id: 11
|
||||||
title: testbucket11
|
title: testbucket11
|
||||||
project_id: 11
|
project_view_id: 44
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 12
|
- id: 12
|
||||||
title: testbucket13
|
title: testbucket13
|
||||||
project_id: 12
|
project_view_id: 48
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 13
|
- id: 13
|
||||||
title: testbucket13
|
title: testbucket13
|
||||||
project_id: 13
|
project_view_id: 52
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 14
|
- id: 14
|
||||||
title: testbucket14
|
title: testbucket14
|
||||||
project_id: 14
|
project_view_id: 56
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 15
|
- id: 15
|
||||||
title: testbucket15
|
title: testbucket15
|
||||||
project_id: 15
|
project_view_id: 60
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 16
|
- id: 16
|
||||||
title: testbucket16
|
title: testbucket16
|
||||||
project_id: 16
|
project_view_id: 64
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 17
|
- id: 17
|
||||||
title: testbucket17
|
title: testbucket17
|
||||||
project_id: 17
|
project_view_id: 68
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 18
|
- id: 18
|
||||||
title: testbucket18
|
title: testbucket18
|
||||||
project_id: 5
|
project_view_id: 20
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 19
|
- id: 19
|
||||||
title: testbucket19
|
title: testbucket19
|
||||||
project_id: 21
|
project_view_id: 84
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 20
|
- id: 20
|
||||||
title: testbucket20
|
title: testbucket20
|
||||||
project_id: 22
|
project_view_id: 88
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 21
|
- id: 21
|
||||||
title: testbucket21
|
title: testbucket21
|
||||||
project_id: 3
|
project_view_id: 12
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
# Duplicate buckets to make deletion of one of them possible
|
# Duplicate buckets to make deletion of one of them possible
|
||||||
- id: 22
|
- id: 22
|
||||||
title: testbucket22
|
title: testbucket22
|
||||||
project_id: 6
|
project_view_id: 24
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
position: 2
|
position: 2
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 23
|
- id: 23
|
||||||
title: testbucket23
|
title: testbucket23
|
||||||
project_id: 7
|
project_view_id: 28
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 24
|
- id: 24
|
||||||
title: testbucket24
|
title: testbucket24
|
||||||
project_id: 8
|
project_view_id: 32
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 25
|
- id: 25
|
||||||
title: testbucket25
|
title: testbucket25
|
||||||
project_id: 9
|
project_view_id: 36
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 26
|
- id: 26
|
||||||
title: testbucket26
|
title: testbucket26
|
||||||
project_id: 10
|
project_view_id: 40
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 27
|
- id: 27
|
||||||
title: testbucket27
|
title: testbucket27
|
||||||
project_id: 11
|
project_view_id: 44
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 28
|
- id: 28
|
||||||
title: testbucket28
|
title: testbucket28
|
||||||
project_id: 12
|
project_view_id: 48
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 29
|
- id: 29
|
||||||
title: testbucket29
|
title: testbucket29
|
||||||
project_id: 13
|
project_view_id: 52
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 30
|
- id: 30
|
||||||
title: testbucket30
|
title: testbucket30
|
||||||
project_id: 14
|
project_view_id: 56
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 31
|
- id: 31
|
||||||
title: testbucket31
|
title: testbucket31
|
||||||
project_id: 15
|
project_view_id: 60
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 32
|
- id: 32
|
||||||
title: testbucket32
|
title: testbucket32
|
||||||
project_id: 16
|
project_view_id: 64
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 33
|
- id: 33
|
||||||
title: testbucket33
|
title: testbucket33
|
||||||
project_id: 17
|
project_view_id: 68
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
# This bucket is the last one in its project
|
# This bucket is the last one in its project
|
||||||
- id: 34
|
- id: 34
|
||||||
title: testbucket34
|
title: testbucket34
|
||||||
project_id: 18
|
project_view_id: 72
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 35
|
- id: 35
|
||||||
title: testbucket35
|
title: testbucket35
|
||||||
project_id: 23
|
project_view_id: 92
|
||||||
created_by_id: -2
|
created_by_id: -2
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 36
|
- id: 36
|
||||||
title: testbucket36
|
title: testbucket36
|
||||||
project_id: 33
|
project_view_id: 132
|
||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 37
|
- id: 37
|
||||||
title: testbucket37
|
title: testbucket37
|
||||||
project_id: 34
|
project_view_id: 136
|
||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 38
|
- id: 38
|
||||||
title: testbucket36
|
title: testbucket36
|
||||||
project_id: 36
|
project_view_id: 144
|
||||||
created_by_id: 15
|
created_by_id: 15
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 39
|
- id: 39
|
||||||
title: testbucket38
|
title: testbucket38
|
||||||
project_id: 38
|
project_view_id: 152
|
||||||
created_by_id: 15
|
created_by_id: 15
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
updated: 2020-04-18 21:13:52
|
updated: 2020-04-18 21:13:52
|
||||||
- id: 40
|
- id: 40
|
||||||
title: testbucket40
|
title: testbucket40
|
||||||
project_id: 2
|
project_view_id: 8
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
position: 10
|
position: 10
|
||||||
created: 2020-04-18 21:13:52
|
created: 2020-04-18 21:13:52
|
||||||
|
954
pkg/db/fixtures/project_views.yml
Normal file
954
pkg/db/fixtures/project_views.yml
Normal file
@ -0,0 +1,954 @@
|
|||||||
|
- id: 1
|
||||||
|
title: List
|
||||||
|
project_id: 1
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 2
|
||||||
|
title: Gantt
|
||||||
|
project_id: 1
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 3
|
||||||
|
title: Table
|
||||||
|
project_id: 1
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 4
|
||||||
|
title: Kanban
|
||||||
|
project_id: 1
|
||||||
|
view_kind: 3
|
||||||
|
done_bucket_id: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 5
|
||||||
|
title: List
|
||||||
|
project_id: 2
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 6
|
||||||
|
title: Gantt
|
||||||
|
project_id: 2
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 7
|
||||||
|
title: Table
|
||||||
|
project_id: 2
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 8
|
||||||
|
title: Kanban
|
||||||
|
project_id: 2
|
||||||
|
view_kind: 3
|
||||||
|
done_bucket_id: 4
|
||||||
|
default_bucket_id: 40
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 9
|
||||||
|
title: List
|
||||||
|
project_id: 3
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 10
|
||||||
|
title: Gantt
|
||||||
|
project_id: 3
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 11
|
||||||
|
title: Table
|
||||||
|
project_id: 3
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 12
|
||||||
|
title: Kanban
|
||||||
|
project_id: 3
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 13
|
||||||
|
title: List
|
||||||
|
project_id: 4
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 14
|
||||||
|
title: Gantt
|
||||||
|
project_id: 4
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 15
|
||||||
|
title: Table
|
||||||
|
project_id: 4
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 16
|
||||||
|
title: Kanban
|
||||||
|
project_id: 4
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 17
|
||||||
|
title: List
|
||||||
|
project_id: 5
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 18
|
||||||
|
title: Gantt
|
||||||
|
project_id: 5
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 19
|
||||||
|
title: Table
|
||||||
|
project_id: 5
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 20
|
||||||
|
title: Kanban
|
||||||
|
project_id: 5
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 21
|
||||||
|
title: List
|
||||||
|
project_id: 6
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 22
|
||||||
|
title: Gantt
|
||||||
|
project_id: 6
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 23
|
||||||
|
title: Table
|
||||||
|
project_id: 6
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 24
|
||||||
|
title: Kanban
|
||||||
|
project_id: 6
|
||||||
|
view_kind: 3
|
||||||
|
default_bucket_id: 22
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 25
|
||||||
|
title: List
|
||||||
|
project_id: 7
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 26
|
||||||
|
title: Gantt
|
||||||
|
project_id: 7
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 27
|
||||||
|
title: Table
|
||||||
|
project_id: 7
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 28
|
||||||
|
title: Kanban
|
||||||
|
project_id: 7
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 29
|
||||||
|
title: List
|
||||||
|
project_id: 8
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 30
|
||||||
|
title: Gantt
|
||||||
|
project_id: 8
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 31
|
||||||
|
title: Table
|
||||||
|
project_id: 8
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 32
|
||||||
|
title: Kanban
|
||||||
|
project_id: 8
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 33
|
||||||
|
title: List
|
||||||
|
project_id: 9
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 34
|
||||||
|
title: Gantt
|
||||||
|
project_id: 9
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 35
|
||||||
|
title: Table
|
||||||
|
project_id: 9
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 36
|
||||||
|
title: Kanban
|
||||||
|
project_id: 9
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 37
|
||||||
|
title: List
|
||||||
|
project_id: 10
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 38
|
||||||
|
title: Gantt
|
||||||
|
project_id: 10
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 39
|
||||||
|
title: Table
|
||||||
|
project_id: 10
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 40
|
||||||
|
title: Kanban
|
||||||
|
project_id: 10
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 41
|
||||||
|
title: List
|
||||||
|
project_id: 11
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 42
|
||||||
|
title: Gantt
|
||||||
|
project_id: 11
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 43
|
||||||
|
title: Table
|
||||||
|
project_id: 11
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 44
|
||||||
|
title: Kanban
|
||||||
|
project_id: 11
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 45
|
||||||
|
title: List
|
||||||
|
project_id: 12
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 46
|
||||||
|
title: Gantt
|
||||||
|
project_id: 12
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 47
|
||||||
|
title: Table
|
||||||
|
project_id: 12
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 48
|
||||||
|
title: Kanban
|
||||||
|
project_id: 12
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 49
|
||||||
|
title: List
|
||||||
|
project_id: 13
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 50
|
||||||
|
title: Gantt
|
||||||
|
project_id: 13
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 51
|
||||||
|
title: Table
|
||||||
|
project_id: 13
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 52
|
||||||
|
title: Kanban
|
||||||
|
project_id: 13
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 53
|
||||||
|
title: List
|
||||||
|
project_id: 14
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 54
|
||||||
|
title: Gantt
|
||||||
|
project_id: 14
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 55
|
||||||
|
title: Table
|
||||||
|
project_id: 14
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 56
|
||||||
|
title: Kanban
|
||||||
|
project_id: 14
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 57
|
||||||
|
title: List
|
||||||
|
project_id: 15
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 58
|
||||||
|
title: Gantt
|
||||||
|
project_id: 15
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 59
|
||||||
|
title: Table
|
||||||
|
project_id: 15
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 60
|
||||||
|
title: Kanban
|
||||||
|
project_id: 15
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 61
|
||||||
|
title: List
|
||||||
|
project_id: 16
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 62
|
||||||
|
title: Gantt
|
||||||
|
project_id: 16
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 63
|
||||||
|
title: Table
|
||||||
|
project_id: 16
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 64
|
||||||
|
title: Kanban
|
||||||
|
project_id: 16
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 65
|
||||||
|
title: List
|
||||||
|
project_id: 17
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 66
|
||||||
|
title: Gantt
|
||||||
|
project_id: 17
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 67
|
||||||
|
title: Table
|
||||||
|
project_id: 17
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 68
|
||||||
|
title: Kanban
|
||||||
|
project_id: 17
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 69
|
||||||
|
title: List
|
||||||
|
project_id: 18
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 70
|
||||||
|
title: Gantt
|
||||||
|
project_id: 18
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 71
|
||||||
|
title: Table
|
||||||
|
project_id: 18
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 72
|
||||||
|
title: Kanban
|
||||||
|
project_id: 18
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 73
|
||||||
|
title: List
|
||||||
|
project_id: 19
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 74
|
||||||
|
title: Gantt
|
||||||
|
project_id: 19
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 75
|
||||||
|
title: Table
|
||||||
|
project_id: 19
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 76
|
||||||
|
title: Kanban
|
||||||
|
project_id: 19
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 77
|
||||||
|
title: List
|
||||||
|
project_id: 20
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 78
|
||||||
|
title: Gantt
|
||||||
|
project_id: 20
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 79
|
||||||
|
title: Table
|
||||||
|
project_id: 20
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 80
|
||||||
|
title: Kanban
|
||||||
|
project_id: 20
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 81
|
||||||
|
title: List
|
||||||
|
project_id: 21
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 82
|
||||||
|
title: Gantt
|
||||||
|
project_id: 21
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 83
|
||||||
|
title: Table
|
||||||
|
project_id: 21
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 84
|
||||||
|
title: Kanban
|
||||||
|
project_id: 21
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 85
|
||||||
|
title: List
|
||||||
|
project_id: 22
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 86
|
||||||
|
title: Gantt
|
||||||
|
project_id: 22
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 87
|
||||||
|
title: Table
|
||||||
|
project_id: 22
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 88
|
||||||
|
title: Kanban
|
||||||
|
project_id: 22
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 89
|
||||||
|
title: List
|
||||||
|
project_id: 23
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 90
|
||||||
|
title: Gantt
|
||||||
|
project_id: 23
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 91
|
||||||
|
title: Table
|
||||||
|
project_id: 23
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 92
|
||||||
|
title: Kanban
|
||||||
|
project_id: 23
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 93
|
||||||
|
title: List
|
||||||
|
project_id: 24
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 94
|
||||||
|
title: Gantt
|
||||||
|
project_id: 24
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 95
|
||||||
|
title: Table
|
||||||
|
project_id: 24
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 96
|
||||||
|
title: Kanban
|
||||||
|
project_id: 24
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 97
|
||||||
|
title: List
|
||||||
|
project_id: 25
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 98
|
||||||
|
title: Gantt
|
||||||
|
project_id: 25
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 99
|
||||||
|
title: Table
|
||||||
|
project_id: 25
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 100
|
||||||
|
title: Kanban
|
||||||
|
project_id: 25
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 101
|
||||||
|
title: List
|
||||||
|
project_id: 26
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 102
|
||||||
|
title: Gantt
|
||||||
|
project_id: 26
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 103
|
||||||
|
title: Table
|
||||||
|
project_id: 26
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 104
|
||||||
|
title: Kanban
|
||||||
|
project_id: 26
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 105
|
||||||
|
title: List
|
||||||
|
project_id: 27
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 106
|
||||||
|
title: Gantt
|
||||||
|
project_id: 27
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 107
|
||||||
|
title: Table
|
||||||
|
project_id: 27
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 108
|
||||||
|
title: Kanban
|
||||||
|
project_id: 27
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 109
|
||||||
|
title: List
|
||||||
|
project_id: 28
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 110
|
||||||
|
title: Gantt
|
||||||
|
project_id: 28
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 111
|
||||||
|
title: Table
|
||||||
|
project_id: 28
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 112
|
||||||
|
title: Kanban
|
||||||
|
project_id: 28
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 113
|
||||||
|
title: List
|
||||||
|
project_id: 29
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 114
|
||||||
|
title: Gantt
|
||||||
|
project_id: 29
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 115
|
||||||
|
title: Table
|
||||||
|
project_id: 29
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 116
|
||||||
|
title: Kanban
|
||||||
|
project_id: 29
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 117
|
||||||
|
title: List
|
||||||
|
project_id: 30
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 118
|
||||||
|
title: Gantt
|
||||||
|
project_id: 30
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 119
|
||||||
|
title: Table
|
||||||
|
project_id: 30
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 120
|
||||||
|
title: Kanban
|
||||||
|
project_id: 30
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 121
|
||||||
|
title: List
|
||||||
|
project_id: 31
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 122
|
||||||
|
title: Gantt
|
||||||
|
project_id: 31
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 123
|
||||||
|
title: Table
|
||||||
|
project_id: 31
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 124
|
||||||
|
title: Kanban
|
||||||
|
project_id: 31
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 125
|
||||||
|
title: List
|
||||||
|
project_id: 32
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 126
|
||||||
|
title: Gantt
|
||||||
|
project_id: 32
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 127
|
||||||
|
title: Table
|
||||||
|
project_id: 32
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 128
|
||||||
|
title: Kanban
|
||||||
|
project_id: 32
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 129
|
||||||
|
title: List
|
||||||
|
project_id: 33
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 130
|
||||||
|
title: Gantt
|
||||||
|
project_id: 33
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 131
|
||||||
|
title: Table
|
||||||
|
project_id: 33
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 132
|
||||||
|
title: Kanban
|
||||||
|
project_id: 33
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 133
|
||||||
|
title: List
|
||||||
|
project_id: 34
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 134
|
||||||
|
title: Gantt
|
||||||
|
project_id: 34
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 135
|
||||||
|
title: Table
|
||||||
|
project_id: 34
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 136
|
||||||
|
title: Kanban
|
||||||
|
project_id: 34
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 137
|
||||||
|
title: List
|
||||||
|
project_id: 35
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 138
|
||||||
|
title: Gantt
|
||||||
|
project_id: 35
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 139
|
||||||
|
title: Table
|
||||||
|
project_id: 35
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 140
|
||||||
|
title: Kanban
|
||||||
|
project_id: 35
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 141
|
||||||
|
title: List
|
||||||
|
project_id: 36
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 142
|
||||||
|
title: Gantt
|
||||||
|
project_id: 36
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 143
|
||||||
|
title: Table
|
||||||
|
project_id: 36
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 144
|
||||||
|
title: Kanban
|
||||||
|
project_id: 36
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 145
|
||||||
|
title: List
|
||||||
|
project_id: 37
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 146
|
||||||
|
title: Gantt
|
||||||
|
project_id: 37
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 147
|
||||||
|
title: Table
|
||||||
|
project_id: 37
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 148
|
||||||
|
title: Kanban
|
||||||
|
project_id: 37
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
||||||
|
- id: 149
|
||||||
|
title: List
|
||||||
|
project_id: 38
|
||||||
|
view_kind: 0
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 150
|
||||||
|
title: Gantt
|
||||||
|
project_id: 38
|
||||||
|
view_kind: 1
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 151
|
||||||
|
title: Table
|
||||||
|
project_id: 38
|
||||||
|
view_kind: 2
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
- id: 152
|
||||||
|
title: Kanban
|
||||||
|
project_id: 38
|
||||||
|
view_kind: 3
|
||||||
|
updated: '2024-03-18 15:14:13'
|
||||||
|
created: '2018-03-18 15:14:13'
|
||||||
|
bucket_configuration_mode: 1
|
@ -5,7 +5,6 @@
|
|||||||
identifier: test1
|
identifier: test1
|
||||||
owner_id: 1
|
owner_id: 1
|
||||||
position: 3
|
position: 3
|
||||||
done_bucket_id: 3
|
|
||||||
updated: 2018-12-02 15:13:12
|
updated: 2018-12-02 15:13:12
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
-
|
||||||
@ -15,8 +14,6 @@
|
|||||||
identifier: test2
|
identifier: test2
|
||||||
owner_id: 3
|
owner_id: 3
|
||||||
position: 2
|
position: 2
|
||||||
done_bucket_id: 4
|
|
||||||
default_bucket_id: 40
|
|
||||||
updated: 2018-12-02 15:13:12
|
updated: 2018-12-02 15:13:12
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
-
|
||||||
@ -53,7 +50,6 @@
|
|||||||
identifier: test6
|
identifier: test6
|
||||||
owner_id: 6
|
owner_id: 6
|
||||||
position: 6
|
position: 6
|
||||||
default_bucket_id: 22
|
|
||||||
updated: 2018-12-02 15:13:12
|
updated: 2018-12-02 15:13:12
|
||||||
created: 2018-12-01 15:13:12
|
created: 2018-12-01 15:13:12
|
||||||
-
|
-
|
||||||
|
138
pkg/db/fixtures/task_buckets.yml
Normal file
138
pkg/db/fixtures/task_buckets.yml
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
- task_id: 1
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 2
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 3
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 2
|
||||||
|
- task_id: 4
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 2
|
||||||
|
- task_id: 5
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 2
|
||||||
|
- task_id: 6
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 3
|
||||||
|
- task_id: 7
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 3
|
||||||
|
- task_id: 8
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 3
|
||||||
|
- task_id: 9
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 10
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 11
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 12
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 13
|
||||||
|
project_view_id: 8
|
||||||
|
bucket_id: 4
|
||||||
|
- task_id: 14
|
||||||
|
project_view_id: 20
|
||||||
|
bucket_id: 18
|
||||||
|
- task_id: 15
|
||||||
|
project_view_id: 24
|
||||||
|
bucket_id: 6
|
||||||
|
- task_id: 16
|
||||||
|
project_view_id: 28
|
||||||
|
bucket_id: 7
|
||||||
|
- task_id: 17
|
||||||
|
project_view_id: 32
|
||||||
|
bucket_id: 8
|
||||||
|
- task_id: 18
|
||||||
|
project_view_id: 36
|
||||||
|
bucket_id: 9
|
||||||
|
- task_id: 19
|
||||||
|
project_view_id: 40
|
||||||
|
bucket_id: 10
|
||||||
|
- task_id: 20
|
||||||
|
project_view_id: 44
|
||||||
|
bucket_id: 11
|
||||||
|
- task_id: 21
|
||||||
|
project_view_id: 128
|
||||||
|
bucket_id: 12
|
||||||
|
- task_id: 22
|
||||||
|
project_view_id: 132
|
||||||
|
bucket_id: 36
|
||||||
|
- task_id: 23
|
||||||
|
project_view_id: 136
|
||||||
|
bucket_id: 37
|
||||||
|
- task_id: 24
|
||||||
|
project_view_id: 60
|
||||||
|
bucket_id: 15
|
||||||
|
- task_id: 25
|
||||||
|
project_view_id: 64
|
||||||
|
bucket_id: 16
|
||||||
|
- task_id: 26
|
||||||
|
project_view_id: 68
|
||||||
|
bucket_id: 17
|
||||||
|
- task_id: 27
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 28
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 29
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 30
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 31
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 32
|
||||||
|
project_view_id: 12
|
||||||
|
bucket_id: 21
|
||||||
|
- task_id: 33
|
||||||
|
project_view_id: 4
|
||||||
|
bucket_id: 1
|
||||||
|
- task_id: 34
|
||||||
|
project_view_id: 80
|
||||||
|
bucket_id: 5
|
||||||
|
- task_id: 35
|
||||||
|
project_view_id: 84
|
||||||
|
bucket_id: 19
|
||||||
|
- task_id: 36
|
||||||
|
project_view_id: 88
|
||||||
|
bucket_id: 20
|
||||||
|
#- task_id: 37
|
||||||
|
# project_view_id: 8
|
||||||
|
# bucket_id: null
|
||||||
|
#- task_id: 38
|
||||||
|
# project_view_id: 88
|
||||||
|
# bucket_id: null
|
||||||
|
#- task_id: 39
|
||||||
|
# project_view_id: 100
|
||||||
|
# bucket_id: null
|
||||||
|
- task_id: 40
|
||||||
|
project_view_id: 144
|
||||||
|
bucket_id: 38
|
||||||
|
- task_id: 41
|
||||||
|
project_view_id: 144
|
||||||
|
bucket_id: 38
|
||||||
|
- task_id: 42
|
||||||
|
project_view_id: 144
|
||||||
|
bucket_id: 38
|
||||||
|
- task_id: 43
|
||||||
|
project_view_id: 144
|
||||||
|
bucket_id: 38
|
||||||
|
- task_id: 44
|
||||||
|
project_view_id: 152
|
||||||
|
bucket_id: 38
|
||||||
|
- task_id: 45
|
||||||
|
project_view_id: 144
|
||||||
|
bucket_id: 38
|
||||||
|
- task_id: 46
|
||||||
|
project_view_id: 152
|
||||||
|
bucket_id: 38
|
138
pkg/db/fixtures/task_positions.yml
Normal file
138
pkg/db/fixtures/task_positions.yml
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
- task_id: 1
|
||||||
|
project_view_id: 1
|
||||||
|
position: 2
|
||||||
|
- task_id: 2
|
||||||
|
project_view_id: 1
|
||||||
|
position: 4
|
||||||
|
#- task_id: 3
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 4
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 5
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 6
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 7
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 8
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 9
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 10
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 11
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 12
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 13
|
||||||
|
# project_view_id: 2
|
||||||
|
# position: null
|
||||||
|
#- task_id: 14
|
||||||
|
# project_view_id: 5
|
||||||
|
# position: null
|
||||||
|
#- task_id: 15
|
||||||
|
# project_view_id: 6
|
||||||
|
# position: null
|
||||||
|
#- task_id: 16
|
||||||
|
# project_view_id: 7
|
||||||
|
# position: null
|
||||||
|
#- task_id: 17
|
||||||
|
# project_view_id: 8
|
||||||
|
# position: null
|
||||||
|
#- task_id: 18
|
||||||
|
# project_view_id: 9
|
||||||
|
# position: null
|
||||||
|
#- task_id: 19
|
||||||
|
# project_view_id: 10
|
||||||
|
# position: null
|
||||||
|
#- task_id: 20
|
||||||
|
# project_view_id: 11
|
||||||
|
# position: null
|
||||||
|
#- task_id: 21
|
||||||
|
# project_view_id: 32
|
||||||
|
# position: null
|
||||||
|
#- task_id: 22
|
||||||
|
# project_view_id: 33
|
||||||
|
# position: null
|
||||||
|
#- task_id: 23
|
||||||
|
# project_view_id: 34
|
||||||
|
# position: null
|
||||||
|
#- task_id: 24
|
||||||
|
# project_view_id: 15
|
||||||
|
# position: null
|
||||||
|
#- task_id: 25
|
||||||
|
# project_view_id: 16
|
||||||
|
# position: null
|
||||||
|
#- task_id: 26
|
||||||
|
# project_view_id: 17
|
||||||
|
# position: null
|
||||||
|
#- task_id: 27
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 28
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 29
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 30
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 31
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 32
|
||||||
|
# project_view_id: 3
|
||||||
|
# position: null
|
||||||
|
#- task_id: 33
|
||||||
|
# project_view_id: 1
|
||||||
|
# position: null
|
||||||
|
#- task_id: 34
|
||||||
|
# project_view_id: 20
|
||||||
|
# position: null
|
||||||
|
- task_id: 35
|
||||||
|
project_view_id: 21
|
||||||
|
position: 0
|
||||||
|
#- task_id: 36
|
||||||
|
# project_view_id: 22
|
||||||
|
# position: null
|
||||||
|
#- task_id: 37
|
||||||
|
# project_view_id: 2
|
||||||
|
# position: null
|
||||||
|
#- task_id: 38
|
||||||
|
# project_view_id: 22
|
||||||
|
# position: null
|
||||||
|
- task_id: 39
|
||||||
|
project_view_id: 25
|
||||||
|
position: 0
|
||||||
|
- task_id: 40
|
||||||
|
project_view_id: 36
|
||||||
|
position: 39
|
||||||
|
- task_id: 41
|
||||||
|
project_view_id: 36
|
||||||
|
position: 40
|
||||||
|
- task_id: 42
|
||||||
|
project_view_id: 36
|
||||||
|
position: 41
|
||||||
|
- task_id: 43
|
||||||
|
project_view_id: 36
|
||||||
|
position: 42
|
||||||
|
- task_id: 44
|
||||||
|
project_view_id: 38
|
||||||
|
position: 43
|
||||||
|
- task_id: 45
|
||||||
|
project_view_id: 36
|
||||||
|
position: 44
|
||||||
|
- task_id: 46
|
||||||
|
project_view_id: 38
|
||||||
|
position: 45
|
@ -7,8 +7,6 @@
|
|||||||
index: 1
|
index: 1
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 1
|
|
||||||
position: 2
|
|
||||||
- id: 2
|
- id: 2
|
||||||
title: 'task #2 done'
|
title: 'task #2 done'
|
||||||
done: true
|
done: true
|
||||||
@ -17,8 +15,6 @@
|
|||||||
index: 2
|
index: 2
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 1
|
|
||||||
position: 4
|
|
||||||
- id: 3
|
- id: 3
|
||||||
title: 'task #3 high prio'
|
title: 'task #3 high prio'
|
||||||
done: false
|
done: false
|
||||||
@ -28,7 +24,6 @@
|
|||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
priority: 100
|
priority: 100
|
||||||
bucket_id: 2
|
|
||||||
- id: 4
|
- id: 4
|
||||||
title: 'task #4 low prio'
|
title: 'task #4 low prio'
|
||||||
done: false
|
done: false
|
||||||
@ -38,7 +33,6 @@
|
|||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
priority: 1
|
priority: 1
|
||||||
bucket_id: 2
|
|
||||||
- id: 5
|
- id: 5
|
||||||
title: 'task #5 higher due date'
|
title: 'task #5 higher due date'
|
||||||
done: false
|
done: false
|
||||||
@ -48,7 +42,6 @@
|
|||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
due_date: 2018-12-01 03:58:44
|
due_date: 2018-12-01 03:58:44
|
||||||
bucket_id: 2
|
|
||||||
- id: 6
|
- id: 6
|
||||||
title: 'task #6 lower due date'
|
title: 'task #6 lower due date'
|
||||||
done: false
|
done: false
|
||||||
@ -58,7 +51,6 @@
|
|||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
due_date: 2018-11-30 22:25:24
|
due_date: 2018-11-30 22:25:24
|
||||||
bucket_id: 3
|
|
||||||
- id: 7
|
- id: 7
|
||||||
title: 'task #7 with start date'
|
title: 'task #7 with start date'
|
||||||
done: false
|
done: false
|
||||||
@ -68,7 +60,6 @@
|
|||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
start_date: 2018-12-12 07:33:20
|
start_date: 2018-12-12 07:33:20
|
||||||
bucket_id: 3
|
|
||||||
- id: 8
|
- id: 8
|
||||||
title: 'task #8 with end date'
|
title: 'task #8 with end date'
|
||||||
done: false
|
done: false
|
||||||
@ -78,7 +69,6 @@
|
|||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
end_date: 2018-12-13 11:20:00
|
end_date: 2018-12-13 11:20:00
|
||||||
bucket_id: 3
|
|
||||||
- id: 9
|
- id: 9
|
||||||
title: 'task #9 with start and end date'
|
title: 'task #9 with start and end date'
|
||||||
done: false
|
done: false
|
||||||
@ -89,14 +79,12 @@
|
|||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
start_date: 2018-12-12 07:33:20
|
start_date: 2018-12-12 07:33:20
|
||||||
end_date: 2018-12-13 11:20:00
|
end_date: 2018-12-13 11:20:00
|
||||||
bucket_id: 1
|
|
||||||
- id: 10
|
- id: 10
|
||||||
title: 'task #10 basic'
|
title: 'task #10 basic'
|
||||||
done: false
|
done: false
|
||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 1
|
project_id: 1
|
||||||
index: 10
|
index: 10
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 11
|
- id: 11
|
||||||
@ -105,7 +93,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 1
|
project_id: 1
|
||||||
index: 11
|
index: 11
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 12
|
- id: 12
|
||||||
@ -114,7 +101,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 1
|
project_id: 1
|
||||||
index: 12
|
index: 12
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 13
|
- id: 13
|
||||||
@ -123,7 +109,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 2
|
project_id: 2
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 4
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 14
|
- id: 14
|
||||||
@ -132,7 +117,6 @@
|
|||||||
created_by_id: 5
|
created_by_id: 5
|
||||||
project_id: 5
|
project_id: 5
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 18
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 15
|
- id: 15
|
||||||
@ -141,7 +125,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 6
|
project_id: 6
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 6
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 16
|
- id: 16
|
||||||
@ -150,7 +133,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 7
|
project_id: 7
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 7
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 17
|
- id: 17
|
||||||
@ -159,7 +141,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 8
|
project_id: 8
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 8
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 18
|
- id: 18
|
||||||
@ -168,7 +149,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 9
|
project_id: 9
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 9
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 19
|
- id: 19
|
||||||
@ -177,7 +157,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 10
|
project_id: 10
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 10
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 20
|
- id: 20
|
||||||
@ -186,7 +165,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 11
|
project_id: 11
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 11
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 21
|
- id: 21
|
||||||
@ -195,7 +173,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 32
|
project_id: 32
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 12
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 22
|
- id: 22
|
||||||
@ -204,7 +181,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 33
|
project_id: 33
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 36
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 23
|
- id: 23
|
||||||
@ -213,7 +189,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 34
|
project_id: 34
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 37
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 24
|
- id: 24
|
||||||
@ -222,7 +197,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 15
|
project_id: 15
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 15
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 25
|
- id: 25
|
||||||
@ -231,7 +205,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 16
|
project_id: 16
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 16
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 26
|
- id: 26
|
||||||
@ -240,7 +213,6 @@
|
|||||||
created_by_id: 6
|
created_by_id: 6
|
||||||
project_id: 17
|
project_id: 17
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 17
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 27
|
- id: 27
|
||||||
@ -249,7 +221,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 1
|
project_id: 1
|
||||||
index: 12
|
index: 12
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
start_date: 2018-11-30 22:25:24
|
start_date: 2018-11-30 22:25:24
|
||||||
@ -260,7 +231,6 @@
|
|||||||
repeat_after: 3600
|
repeat_after: 3600
|
||||||
project_id: 1
|
project_id: 1
|
||||||
index: 13
|
index: 13
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 29
|
- id: 29
|
||||||
@ -269,7 +239,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 1
|
project_id: 1
|
||||||
index: 14
|
index: 14
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 30
|
- id: 30
|
||||||
@ -278,7 +247,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 1
|
project_id: 1
|
||||||
index: 15
|
index: 15
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 31
|
- id: 31
|
||||||
@ -288,7 +256,6 @@
|
|||||||
project_id: 1
|
project_id: 1
|
||||||
index: 16
|
index: 16
|
||||||
hex_color: f0f0f0
|
hex_color: f0f0f0
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 32
|
- id: 32
|
||||||
@ -297,7 +264,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 3
|
project_id: 3
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 21
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 33
|
- id: 33
|
||||||
@ -307,7 +273,6 @@
|
|||||||
project_id: 1
|
project_id: 1
|
||||||
index: 17
|
index: 17
|
||||||
percent_done: 0.5
|
percent_done: 0.5
|
||||||
bucket_id: 1
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
# This task is forbidden for user1
|
# This task is forbidden for user1
|
||||||
@ -317,7 +282,6 @@
|
|||||||
created_by_id: 13
|
created_by_id: 13
|
||||||
project_id: 20
|
project_id: 20
|
||||||
index: 20
|
index: 20
|
||||||
bucket_id: 5
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 35
|
- id: 35
|
||||||
@ -326,7 +290,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 21
|
project_id: 21
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 19
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
- id: 36
|
- id: 36
|
||||||
@ -335,7 +298,6 @@
|
|||||||
created_by_id: 1
|
created_by_id: 1
|
||||||
project_id: 22
|
project_id: 22
|
||||||
index: 1
|
index: 1
|
||||||
bucket_id: 20
|
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
due_date: 2018-10-30 22:25:24
|
due_date: 2018-10-30 22:25:24
|
||||||
@ -374,8 +336,6 @@
|
|||||||
due_date: 2023-03-01 15:00:00
|
due_date: 2023-03-01 15:00:00
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 38
|
|
||||||
position: 39
|
|
||||||
- id: 41
|
- id: 41
|
||||||
uid: 'uid-caldav-test-parent-task'
|
uid: 'uid-caldav-test-parent-task'
|
||||||
title: 'Parent task for Caldav Test'
|
title: 'Parent task for Caldav Test'
|
||||||
@ -388,8 +348,6 @@
|
|||||||
due_date: 2023-03-01 15:00:00
|
due_date: 2023-03-01 15:00:00
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 38
|
|
||||||
position: 40
|
|
||||||
- id: 42
|
- id: 42
|
||||||
uid: 'uid-caldav-test-parent-task-2'
|
uid: 'uid-caldav-test-parent-task-2'
|
||||||
title: 'Parent task for Caldav Test 2'
|
title: 'Parent task for Caldav Test 2'
|
||||||
@ -402,8 +360,6 @@
|
|||||||
due_date: 2023-03-01 15:00:00
|
due_date: 2023-03-01 15:00:00
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 38
|
|
||||||
position: 41
|
|
||||||
- id: 43
|
- id: 43
|
||||||
uid: 'uid-caldav-test-child-task'
|
uid: 'uid-caldav-test-child-task'
|
||||||
title: 'Child task for Caldav Test'
|
title: 'Child task for Caldav Test'
|
||||||
@ -416,8 +372,6 @@
|
|||||||
due_date: 2023-03-01 15:00:00
|
due_date: 2023-03-01 15:00:00
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 38
|
|
||||||
position: 42
|
|
||||||
- id: 44
|
- id: 44
|
||||||
uid: 'uid-caldav-test-child-task-2'
|
uid: 'uid-caldav-test-child-task-2'
|
||||||
title: 'Child task for Caldav Test '
|
title: 'Child task for Caldav Test '
|
||||||
@ -430,8 +384,6 @@
|
|||||||
due_date: 2023-03-01 15:00:00
|
due_date: 2023-03-01 15:00:00
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 38
|
|
||||||
position: 43
|
|
||||||
- id: 45
|
- id: 45
|
||||||
uid: 'uid-caldav-test-parent-task-another-list'
|
uid: 'uid-caldav-test-parent-task-another-list'
|
||||||
title: 'Parent task for Caldav Test'
|
title: 'Parent task for Caldav Test'
|
||||||
@ -444,8 +396,6 @@
|
|||||||
due_date: 2023-03-01 15:00:00
|
due_date: 2023-03-01 15:00:00
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 38
|
|
||||||
position: 44
|
|
||||||
- id: 46
|
- id: 46
|
||||||
uid: 'uid-caldav-test-child-task-another-list'
|
uid: 'uid-caldav-test-child-task-another-list'
|
||||||
title: 'Child task for Caldav Test '
|
title: 'Child task for Caldav Test '
|
||||||
@ -458,5 +408,3 @@
|
|||||||
due_date: 2023-03-01 15:00:00
|
due_date: 2023-03-01 15:00:00
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
updated: 2018-12-01 01:12:04
|
updated: 2018-12-01 01:12:04
|
||||||
bucket_id: 38
|
|
||||||
position: 45
|
|
@ -52,7 +52,10 @@ func TestBucket(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Run("ReadAll", func(t *testing.T) {
|
t.Run("ReadAll", func(t *testing.T) {
|
||||||
t.Run("Normal", func(t *testing.T) {
|
t.Run("Normal", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(nil, map[string]string{"project": "1"})
|
rec, err := testHandler.testReadAllWithUser(nil, map[string]string{
|
||||||
|
"project": "1",
|
||||||
|
"view": "4",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `testbucket1`)
|
assert.Contains(t, rec.Body.String(), `testbucket1`)
|
||||||
assert.Contains(t, rec.Body.String(), `testbucket2`)
|
assert.Contains(t, rec.Body.String(), `testbucket2`)
|
||||||
@ -63,87 +66,151 @@ func TestBucket(t *testing.T) {
|
|||||||
t.Run("Update", func(t *testing.T) {
|
t.Run("Update", func(t *testing.T) {
|
||||||
t.Run("Normal", func(t *testing.T) {
|
t.Run("Normal", func(t *testing.T) {
|
||||||
// Check the project was loaded successfully afterwards, see testReadOneWithUser
|
// Check the project was loaded successfully afterwards, see testReadOneWithUser
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "1"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "1",
|
||||||
|
"project": "1",
|
||||||
|
"view": "4",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "9999"}, `{"title":"TestLoremIpsum"}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "9999",
|
||||||
|
"project": "1",
|
||||||
|
"view": "4",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||||
})
|
})
|
||||||
t.Run("Empty title", func(t *testing.T) {
|
t.Run("Empty title", func(t *testing.T) {
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "1"}, `{"title":""}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "1",
|
||||||
|
"project": "1",
|
||||||
|
"view": "4",
|
||||||
|
}, `{"title":""}`)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
|
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
|
||||||
})
|
})
|
||||||
t.Run("Rights check", func(t *testing.T) {
|
t.Run("Rights check", func(t *testing.T) {
|
||||||
t.Run("Forbidden", func(t *testing.T) {
|
t.Run("Forbidden", func(t *testing.T) {
|
||||||
// Owned by user13
|
// Owned by user13
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "5"}, `{"title":"TestLoremIpsum"}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "5",
|
||||||
|
"project": "20",
|
||||||
|
"view": "80",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Team readonly", func(t *testing.T) {
|
t.Run("Shared Via Team readonly", func(t *testing.T) {
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "6"}, `{"title":"TestLoremIpsum"}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "6",
|
||||||
|
"project": "6",
|
||||||
|
"view": "24",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "7"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "7",
|
||||||
|
"project": "7",
|
||||||
|
"view": "28",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "8"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "8",
|
||||||
|
"project": "8",
|
||||||
|
"view": "32",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "9"}, `{"title":"TestLoremIpsum"}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "9",
|
||||||
|
"project": "9",
|
||||||
|
"view": "36",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via User write", func(t *testing.T) {
|
t.Run("Shared Via User write", func(t *testing.T) {
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "10"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "10",
|
||||||
|
"project": "10",
|
||||||
|
"view": "40",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "11"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "11",
|
||||||
|
"project": "11",
|
||||||
|
"view": "44",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "12"}, `{"title":"TestLoremIpsum"}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "12",
|
||||||
|
"project": "12",
|
||||||
|
"view": "48",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "13"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "13",
|
||||||
|
"project": "13",
|
||||||
|
"view": "52",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "14"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "14",
|
||||||
|
"project": "14",
|
||||||
|
"view": "56",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "15"}, `{"title":"TestLoremIpsum"}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "15",
|
||||||
|
"project": "15",
|
||||||
|
"view": "60",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "16"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "16",
|
||||||
|
"project": "16",
|
||||||
|
"view": "64",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"bucket": "17"}, `{"title":"TestLoremIpsum"}`)
|
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{
|
||||||
|
"bucket": "17",
|
||||||
|
"project": "17",
|
||||||
|
"view": "68",
|
||||||
|
}, `{"title":"TestLoremIpsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||||
})
|
})
|
||||||
@ -151,7 +218,11 @@ func TestBucket(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("Delete", func(t *testing.T) {
|
t.Run("Delete", func(t *testing.T) {
|
||||||
t.Run("Normal", func(t *testing.T) {
|
t.Run("Normal", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "1", "bucket": "1"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "1",
|
||||||
|
"bucket": "1",
|
||||||
|
"view": "4",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
@ -173,60 +244,104 @@ func TestBucket(t *testing.T) {
|
|||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "7", "bucket": "7"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "7",
|
||||||
|
"bucket": "7",
|
||||||
|
"view": "28",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "8", "bucket": "8"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "8",
|
||||||
|
"bucket": "8",
|
||||||
|
"view": "32",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "9", "bucket": "9"})
|
_, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "9",
|
||||||
|
"bucket": "9",
|
||||||
|
"view": "36",
|
||||||
|
})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via User write", func(t *testing.T) {
|
t.Run("Shared Via User write", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "10", "bucket": "10"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "10",
|
||||||
|
"bucket": "10",
|
||||||
|
"view": "40",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "11", "bucket": "11"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "11",
|
||||||
|
"bucket": "11",
|
||||||
|
"view": "44",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
|
||||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "12", "bucket": "12"})
|
_, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "12",
|
||||||
|
"bucket": "12",
|
||||||
|
"view": "48",
|
||||||
|
})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "13", "bucket": "13"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "13",
|
||||||
|
"bucket": "13",
|
||||||
|
"view": "52",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "14", "bucket": "14"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "14",
|
||||||
|
"bucket": "14",
|
||||||
|
"view": "56",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
|
||||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "15", "bucket": "15"})
|
_, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "15",
|
||||||
|
"bucket": "15",
|
||||||
|
"view": "60",
|
||||||
|
})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "16", "bucket": "16"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "16",
|
||||||
|
"bucket": "16",
|
||||||
|
"view": "64",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
|
||||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"project": "17", "bucket": "17"})
|
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{
|
||||||
|
"project": "17",
|
||||||
|
"bucket": "17",
|
||||||
|
"view": "68",
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||||
})
|
})
|
||||||
@ -315,13 +430,16 @@ func TestBucket(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
t.Run("Link Share", func(t *testing.T) {
|
t.Run("Link Share", func(t *testing.T) {
|
||||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"project": "2"}, `{"title":"Lorem Ipsum"}`)
|
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{
|
||||||
|
"project": "2",
|
||||||
|
"view": "8",
|
||||||
|
}, `{"title":"Lorem Ipsum"}`)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||||
db.AssertExists(t, "buckets", map[string]interface{}{
|
db.AssertExists(t, "buckets", map[string]interface{}{
|
||||||
"project_id": 2,
|
"project_view_id": 8,
|
||||||
"created_by_id": -2,
|
"created_by_id": -2,
|
||||||
"title": "Lorem Ipsum",
|
"title": "Lorem Ipsum",
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -115,49 +115,49 @@ func TestTaskCollection(t *testing.T) {
|
|||||||
t.Run("by priority", func(t *testing.T) {
|
t.Run("by priority", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||||
})
|
})
|
||||||
t.Run("by priority desc", func(t *testing.T) {
|
t.Run("by priority desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||||
})
|
})
|
||||||
t.Run("by priority asc", func(t *testing.T) {
|
t.Run("by priority asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||||
})
|
})
|
||||||
// should equal duedate asc
|
// should equal duedate asc
|
||||||
t.Run("by due_date", func(t *testing.T) {
|
t.Run("by due_date", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate desc", func(t *testing.T) {
|
t.Run("by duedate desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||||
})
|
})
|
||||||
// Due date without unix suffix
|
// Due date without unix suffix
|
||||||
t.Run("by duedate asc without suffix", func(t *testing.T) {
|
t.Run("by duedate asc without suffix", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||||
})
|
})
|
||||||
t.Run("by due_date without suffix", func(t *testing.T) {
|
t.Run("by due_date without suffix", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate desc without suffix", func(t *testing.T) {
|
t.Run("by duedate desc without suffix", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate asc", func(t *testing.T) {
|
t.Run("by duedate asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||||
})
|
})
|
||||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||||
@ -358,33 +358,33 @@ func TestTaskCollection(t *testing.T) {
|
|||||||
t.Run("by priority", func(t *testing.T) {
|
t.Run("by priority", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"reactions":null,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"reactions":null,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||||
})
|
})
|
||||||
t.Run("by priority desc", func(t *testing.T) {
|
t.Run("by priority desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||||
})
|
})
|
||||||
t.Run("by priority asc", func(t *testing.T) {
|
t.Run("by priority asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"reactions":null,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":2,"kanban_position":0,"reactions":null,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":19,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":35,"title":"task #35","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":21,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":[{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"labels":[{"id":4,"title":"Label #4 - visible via other task","description":"","hex_color":"","created_by":{"id":2,"name":"","username":"user2","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"},"created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}],"hex_color":"","percent_done":0,"identifier":"test21-1","index":1,"related_tasks":{"related":[{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":null},{"id":1,"title":"task #1","description":"Lorem Ipsum","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"","index":1,"related_tasks":null,"attachments":null,"cover_image_attachment_id":0,"is_favorite":true,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":null}]},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||||
})
|
})
|
||||||
// should equal duedate asc
|
// should equal duedate asc
|
||||||
t.Run("by due_date", func(t *testing.T) {
|
t.Run("by due_date", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate desc", func(t *testing.T) {
|
t.Run("by duedate desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||||
})
|
})
|
||||||
t.Run("by duedate asc", func(t *testing.T) {
|
t.Run("by duedate asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil)
|
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||||
})
|
})
|
||||||
t.Run("invalid parameter", func(t *testing.T) {
|
t.Run("invalid parameter", func(t *testing.T) {
|
||||||
// Invalid parameter should not sort at all
|
// Invalid parameter should not sort at all
|
||||||
|
@ -317,7 +317,7 @@ func TestTask(t *testing.T) {
|
|||||||
t.Run("Different Project", func(t *testing.T) {
|
t.Run("Different Project", func(t *testing.T) {
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":4}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":4}`)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToProject)
|
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||||
})
|
})
|
||||||
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
||||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":9999}`)
|
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"bucket_id":9999}`)
|
||||||
|
148
pkg/migration/20240313230538.go
Normal file
148
pkg/migration/20240313230538.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type projectViewBucketConfiguration20240313230538 struct {
|
||||||
|
Title string
|
||||||
|
Filter string
|
||||||
|
}
|
||||||
|
|
||||||
|
type projectView20240313230538 struct {
|
||||||
|
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||||
|
Title string `xorm:"varchar(255) not null" json:"title" valid:"runelength(1|250)"`
|
||||||
|
ProjectID int64 `xorm:"not null index" json:"project_id" param:"project"`
|
||||||
|
ViewKind int `xorm:"not null" json:"view_kind"`
|
||||||
|
Filter string `xorm:"text null default null" query:"filter" json:"filter"`
|
||||||
|
Position float64 `xorm:"double null" json:"position"`
|
||||||
|
|
||||||
|
BucketConfigurationMode int `xorm:"default 0" json:"bucket_configuration_mode"`
|
||||||
|
BucketConfiguration []*projectViewBucketConfiguration20240313230538 `xorm:"json" json:"bucket_configuration"`
|
||||||
|
|
||||||
|
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||||
|
Created time.Time `xorm:"created not null" json:"created"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (projectView20240313230538) TableName() string {
|
||||||
|
return "project_views"
|
||||||
|
}
|
||||||
|
|
||||||
|
type projects20240313230538 struct {
|
||||||
|
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (projects20240313230538) TableName() string {
|
||||||
|
return "projects"
|
||||||
|
}
|
||||||
|
|
||||||
|
type filters20240313230538 struct {
|
||||||
|
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filters20240313230538) TableName() string {
|
||||||
|
return "saved_filters"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20240313230538",
|
||||||
|
Description: "Add project views table",
|
||||||
|
Migrate: func(tx *xorm.Engine) error {
|
||||||
|
err := tx.Sync2(projectView20240313230538{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
projects := []*projects20240313230538{}
|
||||||
|
err = tx.Find(&projects)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
createView := func(projectID int64, kind int, title string, position float64) error {
|
||||||
|
view := &projectView20240313230538{
|
||||||
|
Title: title,
|
||||||
|
ProjectID: projectID,
|
||||||
|
ViewKind: kind,
|
||||||
|
Position: position,
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == 3 {
|
||||||
|
view.BucketConfigurationMode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := tx.Insert(view)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, project := range projects {
|
||||||
|
err = createView(project.ID, 0, "List", 100)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = createView(project.ID, 1, "Gantt", 200)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = createView(project.ID, 2, "Table", 300)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = createView(project.ID, 3, "Kanban", 400)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filters := []*filters20240313230538{}
|
||||||
|
err = tx.Find(&filters)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filter := range filters {
|
||||||
|
err = createView(filter.ID*-1-1, 0, "List", 100)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = createView(filter.ID*-1-1, 1, "Gantt", 200)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = createView(filter.ID*-1-1, 2, "Table", 300)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = createView(filter.ID*-1-1, 3, "Kanban", 400)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
200
pkg/migration/20240314214802.go
Normal file
200
pkg/migration/20240314214802.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type taskPositions20240314214802 struct {
|
||||||
|
TaskID int64 `xorm:"bigint not null index" json:"task_id"`
|
||||||
|
ProjectViewID int64 `xorm:"bigint not null index" json:"project_view_id"`
|
||||||
|
Position float64 `xorm:"double not null" json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (taskPositions20240314214802) TableName() string {
|
||||||
|
return "task_positions"
|
||||||
|
}
|
||||||
|
|
||||||
|
type task20240314214802 struct {
|
||||||
|
ID int64 `xorm:"bigint autoincr not null unique pk"`
|
||||||
|
ProjectID int64 `xorm:"bigint INDEX not null"`
|
||||||
|
Position float64 `xorm:"double not null"`
|
||||||
|
KanbanPosition float64 `xorm:"double not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (task20240314214802) TableName() string {
|
||||||
|
return "tasks"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20240314214802",
|
||||||
|
Description: "make task position separate",
|
||||||
|
Migrate: func(tx *xorm.Engine) error {
|
||||||
|
err := tx.Sync2(taskPositions20240314214802{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := []*task20240314214802{}
|
||||||
|
err = tx.Find(&tasks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
views := []*projectView20240313230538{}
|
||||||
|
err = tx.Find(&views)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap := make(map[int64][]*projectView20240313230538)
|
||||||
|
for _, view := range views {
|
||||||
|
if _, has := viewMap[view.ProjectID]; !has {
|
||||||
|
viewMap[view.ProjectID] = []*projectView20240313230538{}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap[view.ProjectID] = append(viewMap[view.ProjectID], view)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
for _, view := range viewMap[task.ProjectID] {
|
||||||
|
if view.ViewKind == 0 { // List view
|
||||||
|
position := &taskPositions20240314214802{
|
||||||
|
TaskID: task.ID,
|
||||||
|
Position: task.Position,
|
||||||
|
ProjectViewID: view.ID,
|
||||||
|
}
|
||||||
|
_, err = tx.Insert(position)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if view.ViewKind == 3 { // Kanban view
|
||||||
|
position := &taskPositions20240314214802{
|
||||||
|
TaskID: task.ID,
|
||||||
|
Position: task.KanbanPosition,
|
||||||
|
ProjectViewID: view.ID,
|
||||||
|
}
|
||||||
|
_, err = tx.Insert(position)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DatabaseType.GetString() == "sqlite" {
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
create table tasks_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
primary key autoincrement,
|
||||||
|
title TEXT not null,
|
||||||
|
description TEXT,
|
||||||
|
done INTEGER,
|
||||||
|
done_at DATETIME,
|
||||||
|
due_date DATETIME,
|
||||||
|
project_id INTEGER not null,
|
||||||
|
repeat_after INTEGER,
|
||||||
|
repeat_mode INTEGER default 0 not null,
|
||||||
|
priority INTEGER,
|
||||||
|
start_date DATETIME,
|
||||||
|
end_date DATETIME,
|
||||||
|
hex_color TEXT,
|
||||||
|
percent_done REAL,
|
||||||
|
"index" INTEGER default 0 not null,
|
||||||
|
uid TEXT,
|
||||||
|
cover_image_attachment_id INTEGER default 0,
|
||||||
|
created DATETIME not null,
|
||||||
|
updated DATETIME not null,
|
||||||
|
bucket_id INTEGER,
|
||||||
|
created_by_id INTEGER not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into tasks_dg_tmp(id, title, description, done, done_at, due_date, project_id, repeat_after, repeat_mode,
|
||||||
|
priority, start_date, end_date, hex_color, percent_done, "index", uid,
|
||||||
|
cover_image_attachment_id, created, updated, bucket_id, created_by_id)
|
||||||
|
select id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
done,
|
||||||
|
done_at,
|
||||||
|
due_date,
|
||||||
|
project_id,
|
||||||
|
repeat_after,
|
||||||
|
repeat_mode,
|
||||||
|
priority,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
hex_color,
|
||||||
|
percent_done,
|
||||||
|
"index",
|
||||||
|
uid,
|
||||||
|
cover_image_attachment_id,
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
bucket_id,
|
||||||
|
created_by_id
|
||||||
|
from tasks;
|
||||||
|
|
||||||
|
drop table tasks;
|
||||||
|
|
||||||
|
alter table tasks_dg_tmp
|
||||||
|
rename to tasks;
|
||||||
|
|
||||||
|
create index IDX_tasks_done
|
||||||
|
on tasks (done);
|
||||||
|
|
||||||
|
create index IDX_tasks_done_at
|
||||||
|
on tasks (done_at);
|
||||||
|
|
||||||
|
create index IDX_tasks_due_date
|
||||||
|
on tasks (due_date);
|
||||||
|
|
||||||
|
create index IDX_tasks_end_date
|
||||||
|
on tasks (end_date);
|
||||||
|
|
||||||
|
create index IDX_tasks_project_id
|
||||||
|
on tasks (project_id);
|
||||||
|
|
||||||
|
create index IDX_tasks_repeat_after
|
||||||
|
on tasks (repeat_after);
|
||||||
|
|
||||||
|
create index IDX_tasks_start_date
|
||||||
|
on tasks (start_date);
|
||||||
|
|
||||||
|
create unique index UQE_tasks_id
|
||||||
|
on tasks (id);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dropTableColum(tx, "tasks", "position")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return dropTableColum(tx, "tasks", "kanban_position")
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
119
pkg/migration/20240315093418.go
Normal file
119
pkg/migration/20240315093418.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type buckets20240315093418 struct {
|
||||||
|
ID int64 `xorm:"bigint autoincr not null unique pk"`
|
||||||
|
ProjectID int64 `xorm:"bigint not null"`
|
||||||
|
ProjectViewID int64 `xorm:"bigint not null default 0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (buckets20240315093418) TableName() string {
|
||||||
|
return "buckets"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20240315093418",
|
||||||
|
Description: "Relate buckets to views instead of projects",
|
||||||
|
Migrate: func(tx *xorm.Engine) (err error) {
|
||||||
|
err = tx.Sync2(buckets20240315093418{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buckets := []*buckets20240315093418{}
|
||||||
|
err = tx.Find(&buckets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
views := []*projectView20240313230538{}
|
||||||
|
err = tx.Find(&views)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap := make(map[int64][]*projectView20240313230538)
|
||||||
|
for _, view := range views {
|
||||||
|
if _, has := viewMap[view.ProjectID]; !has {
|
||||||
|
viewMap[view.ProjectID] = []*projectView20240313230538{}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap[view.ProjectID] = append(viewMap[view.ProjectID], view)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
for _, view := range viewMap[bucket.ProjectID] {
|
||||||
|
if view.ViewKind == 3 { // Kanban view
|
||||||
|
|
||||||
|
bucket.ProjectViewID = view.ID
|
||||||
|
|
||||||
|
_, err = tx.
|
||||||
|
Where("id = ?", bucket.ID).
|
||||||
|
Cols("project_view_id").
|
||||||
|
Update(bucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DatabaseType.GetString() == "sqlite" {
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
create table buckets_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
primary key autoincrement,
|
||||||
|
title TEXT not null,
|
||||||
|
"limit" INTEGER default 0,
|
||||||
|
position REAL,
|
||||||
|
created DATETIME not null,
|
||||||
|
updated DATETIME not null,
|
||||||
|
created_by_id INTEGER not null,
|
||||||
|
project_view_id INTEGER not null default 0
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into buckets_dg_tmp(id, title, "limit", position, created, updated, created_by_id, project_view_id)
|
||||||
|
select id, title, "limit", position, created, updated, created_by_id, project_view_id
|
||||||
|
from buckets;
|
||||||
|
|
||||||
|
drop table buckets;
|
||||||
|
|
||||||
|
alter table buckets_dg_tmp
|
||||||
|
rename to buckets;
|
||||||
|
|
||||||
|
create unique index UQE_buckets_id
|
||||||
|
on buckets (id);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dropTableColum(tx, "buckets", "project_id")
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
158
pkg/migration/20240315104205.go
Normal file
158
pkg/migration/20240315104205.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type projects20240315104205 struct {
|
||||||
|
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
|
||||||
|
DefaultBucketID int64 `xorm:"bigint INDEX null" json:"default_bucket_id"`
|
||||||
|
DoneBucketID int64 `xorm:"bigint INDEX null" json:"done_bucket_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (projects20240315104205) TableName() string {
|
||||||
|
return "projects"
|
||||||
|
}
|
||||||
|
|
||||||
|
type projectView20240315104205 struct {
|
||||||
|
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
|
||||||
|
ViewKind int `xorm:"not null" json:"view_kind"`
|
||||||
|
DefaultBucketID int64 `xorm:"bigint INDEX null" json:"default_bucket_id"`
|
||||||
|
DoneBucketID int64 `xorm:"bigint INDEX null" json:"done_bucket_id"`
|
||||||
|
ProjectID int64 `xorm:"not null index" json:"project_id" param:"project"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (projectView20240315104205) TableName() string {
|
||||||
|
return "project_views"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20240315104205",
|
||||||
|
Description: "Move done and default bucket id to views",
|
||||||
|
Migrate: func(tx *xorm.Engine) (err error) {
|
||||||
|
err = tx.Sync(projectView20240315104205{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
projects := []*projects20240315104205{}
|
||||||
|
err = tx.Find(&projects)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
views := []*projectView20240315104205{}
|
||||||
|
err = tx.Find(&views)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap := make(map[int64][]*projectView20240315104205)
|
||||||
|
for _, view := range views {
|
||||||
|
if _, has := viewMap[view.ProjectID]; !has {
|
||||||
|
viewMap[view.ProjectID] = []*projectView20240315104205{}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap[view.ProjectID] = append(viewMap[view.ProjectID], view)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, project := range projects {
|
||||||
|
for _, view := range viewMap[project.ID] {
|
||||||
|
if view.ViewKind == 3 { // Kanban view
|
||||||
|
view.DefaultBucketID = project.DefaultBucketID
|
||||||
|
view.DoneBucketID = project.DoneBucketID
|
||||||
|
_, err = tx.
|
||||||
|
Where("id = ?", view.ID).
|
||||||
|
Cols("default_bucket_id", "done_bucket_id").
|
||||||
|
Update(view)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DatabaseType.GetString() == "sqlite" {
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
create table projects_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
primary key autoincrement,
|
||||||
|
title TEXT not null,
|
||||||
|
description TEXT,
|
||||||
|
identifier TEXT,
|
||||||
|
hex_color TEXT,
|
||||||
|
owner_id INTEGER not null,
|
||||||
|
parent_project_id INTEGER,
|
||||||
|
is_archived INTEGER default 0 not null,
|
||||||
|
background_file_id INTEGER,
|
||||||
|
background_blur_hash TEXT,
|
||||||
|
position REAL,
|
||||||
|
created DATETIME not null,
|
||||||
|
updated DATETIME not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into projects_dg_tmp(id, title, description, identifier, hex_color, owner_id, parent_project_id, is_archived,
|
||||||
|
background_file_id, background_blur_hash, position, created, updated)
|
||||||
|
select id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
identifier,
|
||||||
|
hex_color,
|
||||||
|
owner_id,
|
||||||
|
parent_project_id,
|
||||||
|
is_archived,
|
||||||
|
background_file_id,
|
||||||
|
background_blur_hash,
|
||||||
|
position,
|
||||||
|
created,
|
||||||
|
updated
|
||||||
|
from projects;
|
||||||
|
|
||||||
|
drop table projects;
|
||||||
|
|
||||||
|
alter table projects_dg_tmp
|
||||||
|
rename to projects;
|
||||||
|
|
||||||
|
create index IDX_projects_owner_id
|
||||||
|
on projects (owner_id);
|
||||||
|
|
||||||
|
create index IDX_projects_parent_project_id
|
||||||
|
on projects (parent_project_id);
|
||||||
|
|
||||||
|
create unique index UQE_projects_id
|
||||||
|
on projects (id);
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dropTableColum(tx, "projects", "done_bucket_id")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return dropTableColum(tx, "projects", "default_bucket_id")
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
184
pkg/migration/20240315110428.go
Normal file
184
pkg/migration/20240315110428.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
|
"src.techknowlogick.com/xormigrate"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type task20240315110428 struct {
|
||||||
|
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"projecttask"`
|
||||||
|
BucketID int64 `xorm:"bigint not null"`
|
||||||
|
ProjectID int64 `xorm:"bigint INDEX not null" json:"project_id" param:"project"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (task20240315110428) TableName() string {
|
||||||
|
return "tasks"
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskBuckets20240315110428 struct {
|
||||||
|
BucketID int64 `xorm:"bigint not null index"`
|
||||||
|
TaskID int64 `xorm:"bigint not null index"`
|
||||||
|
ProjectViewID int64 `xorm:"bigint not null index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (taskBuckets20240315110428) TableName() string {
|
||||||
|
return "task_buckets"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
migrations = append(migrations, &xormigrate.Migration{
|
||||||
|
ID: "20240315110428",
|
||||||
|
Description: "",
|
||||||
|
Migrate: func(tx *xorm.Engine) (err error) {
|
||||||
|
err = tx.Sync2(taskBuckets20240315110428{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := []*task20240315110428{}
|
||||||
|
err = tx.Find(&tasks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
views := []*projectView20240313230538{}
|
||||||
|
err = tx.Find(&views)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap := make(map[int64][]*projectView20240313230538)
|
||||||
|
for _, view := range views {
|
||||||
|
if _, has := viewMap[view.ProjectID]; !has {
|
||||||
|
viewMap[view.ProjectID] = []*projectView20240313230538{}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap[view.ProjectID] = append(viewMap[view.ProjectID], view)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
for _, view := range viewMap[task.ProjectID] {
|
||||||
|
if view.ViewKind == 3 { // Kanban view
|
||||||
|
|
||||||
|
pos := taskBuckets20240315110428{
|
||||||
|
TaskID: task.ID,
|
||||||
|
BucketID: task.BucketID,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Insert(pos)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DatabaseType.GetString() == "sqlite" {
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
create table tasks_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER not null
|
||||||
|
primary key autoincrement,
|
||||||
|
title TEXT not null,
|
||||||
|
description TEXT,
|
||||||
|
done INTEGER,
|
||||||
|
done_at DATETIME,
|
||||||
|
due_date DATETIME,
|
||||||
|
project_id INTEGER not null,
|
||||||
|
repeat_after INTEGER,
|
||||||
|
repeat_mode INTEGER default 0 not null,
|
||||||
|
priority INTEGER,
|
||||||
|
start_date DATETIME,
|
||||||
|
end_date DATETIME,
|
||||||
|
hex_color TEXT,
|
||||||
|
percent_done REAL,
|
||||||
|
"index" INTEGER default 0 not null,
|
||||||
|
uid TEXT,
|
||||||
|
cover_image_attachment_id INTEGER default 0,
|
||||||
|
created DATETIME not null,
|
||||||
|
updated DATETIME not null,
|
||||||
|
created_by_id INTEGER not null
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into tasks_dg_tmp(id, title, description, done, done_at, due_date, project_id, repeat_after, repeat_mode,
|
||||||
|
priority, start_date, end_date, hex_color, percent_done, "index", uid,
|
||||||
|
cover_image_attachment_id, created, updated, created_by_id)
|
||||||
|
select id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
done,
|
||||||
|
done_at,
|
||||||
|
due_date,
|
||||||
|
project_id,
|
||||||
|
repeat_after,
|
||||||
|
repeat_mode,
|
||||||
|
priority,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
hex_color,
|
||||||
|
percent_done,
|
||||||
|
"index",
|
||||||
|
uid,
|
||||||
|
cover_image_attachment_id,
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
created_by_id
|
||||||
|
from tasks;
|
||||||
|
|
||||||
|
drop table tasks;
|
||||||
|
|
||||||
|
alter table tasks_dg_tmp
|
||||||
|
rename to tasks;
|
||||||
|
|
||||||
|
create index IDX_tasks_done
|
||||||
|
on tasks (done);
|
||||||
|
|
||||||
|
create index IDX_tasks_done_at
|
||||||
|
on tasks (done_at);
|
||||||
|
|
||||||
|
create index IDX_tasks_due_date
|
||||||
|
on tasks (due_date);
|
||||||
|
|
||||||
|
create index IDX_tasks_end_date
|
||||||
|
on tasks (end_date);
|
||||||
|
|
||||||
|
create index IDX_tasks_project_id
|
||||||
|
on tasks (project_id);
|
||||||
|
|
||||||
|
create index IDX_tasks_repeat_after
|
||||||
|
on tasks (repeat_after);
|
||||||
|
|
||||||
|
create index IDX_tasks_start_date
|
||||||
|
on tasks (start_date);
|
||||||
|
|
||||||
|
create unique index UQE_tasks_id
|
||||||
|
on tasks (id);
|
||||||
|
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dropTableColum(tx, "tasks", "bucket_id")
|
||||||
|
},
|
||||||
|
Rollback: func(tx *xorm.Engine) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -412,6 +412,33 @@ func (err *ErrCannotArchiveDefaultProject) HTTPError() web.HTTPError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrProjectViewDoesNotExist represents an error where the default project is being deleted
|
||||||
|
type ErrProjectViewDoesNotExist struct {
|
||||||
|
ProjectViewID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrProjectViewDoesNotExist checks if an error is a project is archived error.
|
||||||
|
func IsErrProjectViewDoesNotExist(err error) bool {
|
||||||
|
_, ok := err.(*ErrProjectViewDoesNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *ErrProjectViewDoesNotExist) Error() string {
|
||||||
|
return fmt.Sprintf("Project view does not exist [ProjectViewID: %d]", err.ProjectViewID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCodeProjectViewDoesNotExist holds the unique world-error code of this error
|
||||||
|
const ErrCodeProjectViewDoesNotExist = 3014
|
||||||
|
|
||||||
|
// HTTPError holds the http error description
|
||||||
|
func (err *ErrProjectViewDoesNotExist) HTTPError() web.HTTPError {
|
||||||
|
return web.HTTPError{
|
||||||
|
HTTPCode: http.StatusNotFound,
|
||||||
|
Code: ErrCodeProjectViewDoesNotExist,
|
||||||
|
Message: "This project view does not exist.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==============
|
// ==============
|
||||||
// Task errors
|
// Task errors
|
||||||
// ==============
|
// ==============
|
||||||
@ -1087,6 +1114,25 @@ func (err ErrInvalidReactionEntityKind) HTTPError() web.HTTPError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrMustHaveProjectViewToSortByPosition represents an error where no project view id was supplied
|
||||||
|
type ErrMustHaveProjectViewToSortByPosition struct{}
|
||||||
|
|
||||||
|
func (err ErrMustHaveProjectViewToSortByPosition) Error() string {
|
||||||
|
return "You must provide a project view ID when sorting by position"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCodeMustHaveProjectViewToSortByPosition holds the unique world-error code of this error
|
||||||
|
const ErrCodeMustHaveProjectViewToSortByPosition = 4026
|
||||||
|
|
||||||
|
// HTTPError holds the http error description
|
||||||
|
func (err ErrMustHaveProjectViewToSortByPosition) HTTPError() web.HTTPError {
|
||||||
|
return web.HTTPError{
|
||||||
|
HTTPCode: http.StatusBadRequest,
|
||||||
|
Code: ErrCodeMustHaveProjectViewToSortByPosition,
|
||||||
|
Message: "You must provide a project view ID when sorting by position",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Team errors
|
// Team errors
|
||||||
// ============
|
// ============
|
||||||
@ -1481,27 +1527,27 @@ func (err ErrBucketDoesNotExist) HTTPError() web.HTTPError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrBucketDoesNotBelongToProject represents an error where a kanban bucket does not belong to a project
|
// ErrBucketDoesNotBelongToProjectView represents an error where a kanban bucket does not belong to a project
|
||||||
type ErrBucketDoesNotBelongToProject struct {
|
type ErrBucketDoesNotBelongToProjectView struct {
|
||||||
BucketID int64
|
BucketID int64
|
||||||
ProjectID int64
|
ProjectViewID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrBucketDoesNotBelongToProject checks if an error is ErrBucketDoesNotBelongToProject.
|
// IsErrBucketDoesNotBelongToProject checks if an error is ErrBucketDoesNotBelongToProjectView.
|
||||||
func IsErrBucketDoesNotBelongToProject(err error) bool {
|
func IsErrBucketDoesNotBelongToProject(err error) bool {
|
||||||
_, ok := err.(ErrBucketDoesNotBelongToProject)
|
_, ok := err.(ErrBucketDoesNotBelongToProjectView)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrBucketDoesNotBelongToProject) Error() string {
|
func (err ErrBucketDoesNotBelongToProjectView) Error() string {
|
||||||
return fmt.Sprintf("Bucket does not not belong to project [BucketID: %d, ProjectID: %d]", err.BucketID, err.ProjectID)
|
return fmt.Sprintf("Bucket does not not belong to project view [BucketID: %d, ProjectViewID: %d]", err.BucketID, err.ProjectViewID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrCodeBucketDoesNotBelongToProject holds the unique world-error code of this error
|
// ErrCodeBucketDoesNotBelongToProject holds the unique world-error code of this error
|
||||||
const ErrCodeBucketDoesNotBelongToProject = 10002
|
const ErrCodeBucketDoesNotBelongToProject = 10002
|
||||||
|
|
||||||
// HTTPError holds the http error description
|
// HTTPError holds the http error description
|
||||||
func (err ErrBucketDoesNotBelongToProject) HTTPError() web.HTTPError {
|
func (err ErrBucketDoesNotBelongToProjectView) HTTPError() web.HTTPError {
|
||||||
return web.HTTPError{
|
return web.HTTPError{
|
||||||
HTTPCode: http.StatusBadRequest,
|
HTTPCode: http.StatusBadRequest,
|
||||||
Code: ErrCodeBucketDoesNotBelongToProject,
|
Code: ErrCodeBucketDoesNotBelongToProject,
|
||||||
@ -1511,8 +1557,8 @@ func (err ErrBucketDoesNotBelongToProject) HTTPError() web.HTTPError {
|
|||||||
|
|
||||||
// ErrCannotRemoveLastBucket represents an error where a kanban bucket is the last on a project and thus cannot be removed.
|
// ErrCannotRemoveLastBucket represents an error where a kanban bucket is the last on a project and thus cannot be removed.
|
||||||
type ErrCannotRemoveLastBucket struct {
|
type ErrCannotRemoveLastBucket struct {
|
||||||
BucketID int64
|
BucketID int64
|
||||||
ProjectID int64
|
ProjectViewID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrCannotRemoveLastBucket checks if an error is ErrCannotRemoveLastBucket.
|
// IsErrCannotRemoveLastBucket checks if an error is ErrCannotRemoveLastBucket.
|
||||||
@ -1522,7 +1568,7 @@ func IsErrCannotRemoveLastBucket(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrCannotRemoveLastBucket) Error() string {
|
func (err ErrCannotRemoveLastBucket) Error() string {
|
||||||
return fmt.Sprintf("Cannot remove last bucket of project [BucketID: %d, ProjectID: %d]", err.BucketID, err.ProjectID)
|
return fmt.Sprintf("Cannot remove last bucket of project view [BucketID: %d, ProjectViewID: %d]", err.BucketID, err.ProjectViewID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrCodeCannotRemoveLastBucket holds the unique world-error code of this error
|
// ErrCodeCannotRemoveLastBucket holds the unique world-error code of this error
|
||||||
@ -1533,7 +1579,7 @@ func (err ErrCannotRemoveLastBucket) HTTPError() web.HTTPError {
|
|||||||
return web.HTTPError{
|
return web.HTTPError{
|
||||||
HTTPCode: http.StatusPreconditionFailed,
|
HTTPCode: http.StatusPreconditionFailed,
|
||||||
Code: ErrCodeCannotRemoveLastBucket,
|
Code: ErrCodeCannotRemoveLastBucket,
|
||||||
Message: "You cannot remove the last bucket on this project.",
|
Message: "You cannot remove the last bucket on this project view.",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (task
|
|||||||
tasks, _, _, err := getTasksForProjects(s, rawProjects, u, &taskSearchOptions{
|
tasks, _, _, err := getTasksForProjects(s, rawProjects, u, &taskSearchOptions{
|
||||||
page: 0,
|
page: 0,
|
||||||
perPage: -1,
|
perPage: -1,
|
||||||
})
|
}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return taskIDs, err
|
return taskIDs, err
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,9 @@ type Bucket struct {
|
|||||||
// The title of this bucket.
|
// The title of this bucket.
|
||||||
Title string `xorm:"text not null" valid:"required" minLength:"1" json:"title"`
|
Title string `xorm:"text not null" valid:"required" minLength:"1" json:"title"`
|
||||||
// The project this bucket belongs to.
|
// The project this bucket belongs to.
|
||||||
ProjectID int64 `xorm:"bigint not null" json:"project_id" param:"project"`
|
ProjectID int64 `xorm:"-" json:"-" param:"project"`
|
||||||
|
// The project view this bucket belongs to.
|
||||||
|
ProjectViewID int64 `xorm:"bigint not null" json:"project_view_id" param:"view"`
|
||||||
// All tasks which belong to this bucket.
|
// All tasks which belong to this bucket.
|
||||||
Tasks []*Task `xorm:"-" json:"tasks"`
|
Tasks []*Task `xorm:"-" json:"tasks"`
|
||||||
|
|
||||||
@ -68,6 +70,16 @@ func (b *Bucket) TableName() string {
|
|||||||
return "buckets"
|
return "buckets"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskBucket struct {
|
||||||
|
BucketID int64 `xorm:"bigint not null index"`
|
||||||
|
TaskID int64 `xorm:"bigint not null index"`
|
||||||
|
ProjectViewID int64 `xorm:"bigint not null index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TaskBucket) TableName() string {
|
||||||
|
return "task_buckets"
|
||||||
|
}
|
||||||
|
|
||||||
func getBucketByID(s *xorm.Session, id int64) (b *Bucket, err error) {
|
func getBucketByID(s *xorm.Session, id int64) (b *Bucket, err error) {
|
||||||
b = &Bucket{}
|
b = &Bucket{}
|
||||||
exists, err := s.Where("id = ?", id).Get(b)
|
exists, err := s.Where("id = ?", id).Get(b)
|
||||||
@ -80,14 +92,14 @@ func getBucketByID(s *xorm.Session, id int64) (b *Bucket, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultBucketID(s *xorm.Session, project *Project) (bucketID int64, err error) {
|
func getDefaultBucketID(s *xorm.Session, view *ProjectView) (bucketID int64, err error) {
|
||||||
if project.DefaultBucketID != 0 {
|
if view.DefaultBucketID != 0 {
|
||||||
return project.DefaultBucketID, nil
|
return view.DefaultBucketID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket := &Bucket{}
|
bucket := &Bucket{}
|
||||||
_, err = s.
|
_, err = s.
|
||||||
Where("project_id = ?", project.ID).
|
Where("project_view_id = ?", view.ID).
|
||||||
OrderBy("position asc").
|
OrderBy("position asc").
|
||||||
Get(bucket)
|
Get(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -97,31 +109,26 @@ func getDefaultBucketID(s *xorm.Session, project *Project) (bucketID int64, err
|
|||||||
return bucket.ID, nil
|
return bucket.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadAll returns all buckets with their tasks for a certain project
|
// ReadAll returns all manual buckets for a certain project
|
||||||
// @Summary Get all kanban buckets of a project
|
// @Summary Get all kanban buckets of a project
|
||||||
// @Description Returns all kanban buckets with belong to a project including their tasks. Buckets are always sorted by their `position` in ascending order. Tasks are sorted by their `kanban_position` in ascending order.
|
// @Description Returns all kanban buckets which belong to that project. Buckets are always sorted by their `position` in ascending order. To get all buckets with their tasks, use the tasks endpoint with a kanban view.
|
||||||
// @tags project
|
// @tags project
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security JWTKeyAuth
|
// @Security JWTKeyAuth
|
||||||
// @Param id path int true "Project Id"
|
// @Param id path int true "Project ID"
|
||||||
// @Param page query int false "The page number for tasks. Used for pagination. If not provided, the first page of results is returned."
|
// @Param view path int true "Project view ID"
|
||||||
// @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page."
|
// @Success 200 {array} models.Bucket "The buckets"
|
||||||
// @Param s query string false "Search tasks by task text."
|
|
||||||
// @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature."
|
|
||||||
// @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)"
|
|
||||||
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
|
|
||||||
// @Success 200 {array} models.Bucket "The buckets with their tasks"
|
|
||||||
// @Failure 500 {object} models.Message "Internal server error"
|
// @Failure 500 {object} models.Message "Internal server error"
|
||||||
// @Router /projects/{id}/buckets [get]
|
// @Router /projects/{id}/views/{view}/buckets [get]
|
||||||
func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, _ string, _ int, _ int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||||
|
|
||||||
project, err := GetProjectSimpleByID(s, b.ProjectID)
|
view, err := GetProjectViewByIDAndProject(s, b.ProjectViewID, b.ProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
can, _, err := project.CanRead(s, auth)
|
can, _, err := view.CanRead(s, auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
@ -129,16 +136,61 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||||||
return nil, 0, 0, ErrGenericForbidden{}
|
return nil, 0, 0, ErrGenericForbidden{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all buckets for this project
|
|
||||||
buckets := []*Bucket{}
|
buckets := []*Bucket{}
|
||||||
err = s.
|
err = s.
|
||||||
Where("project_id = ?", b.ProjectID).
|
Where("project_view_id = ?", b.ProjectViewID).
|
||||||
OrderBy("position").
|
OrderBy("position").
|
||||||
Find(&buckets)
|
Find(&buckets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userIDs := make([]int64, 0, len(buckets))
|
||||||
|
for _, bb := range buckets {
|
||||||
|
userIDs = append(userIDs, bb.CreatedByID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all users
|
||||||
|
users, err := getUsersOrLinkSharesFromIDs(s, userIDs)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bb := range buckets {
|
||||||
|
bb.CreatedBy = users[bb.CreatedByID]
|
||||||
|
}
|
||||||
|
|
||||||
|
return buckets, len(buckets), int64(len(buckets)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Project, opts *taskSearchOptions, auth web.Auth) (bucketsWithTasks []*Bucket, err error) {
|
||||||
|
// Get all buckets for this project
|
||||||
|
buckets := []*Bucket{}
|
||||||
|
|
||||||
|
if view.BucketConfigurationMode == BucketConfigurationModeManual {
|
||||||
|
err = s.
|
||||||
|
Where("project_view_id = ?", view.ID).
|
||||||
|
OrderBy("position").
|
||||||
|
Find(&buckets)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.BucketConfigurationMode == BucketConfigurationModeFilter {
|
||||||
|
for id, bc := range view.BucketConfiguration {
|
||||||
|
buckets = append(buckets, &Bucket{
|
||||||
|
ID: int64(id),
|
||||||
|
Title: bc.Title,
|
||||||
|
ProjectViewID: view.ID,
|
||||||
|
Position: float64(id),
|
||||||
|
CreatedByID: auth.GetID(),
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make a map from the bucket slice with their id as key so that we can use it to put the tasks in their buckets
|
// Make a map from the bucket slice with their id as key so that we can use it to put the tasks in their buckets
|
||||||
bucketMap := make(map[int64]*Bucket, len(buckets))
|
bucketMap := make(map[int64]*Bucket, len(buckets))
|
||||||
userIDs := make([]int64, 0, len(buckets))
|
userIDs := make([]int64, 0, len(buckets))
|
||||||
@ -159,20 +211,13 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||||||
|
|
||||||
tasks := []*Task{}
|
tasks := []*Task{}
|
||||||
|
|
||||||
opts, err := getTaskFilterOptsFromCollection(&b.TaskCollection)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.sortby = []*sortParam{
|
opts.sortby = []*sortParam{
|
||||||
{
|
{
|
||||||
orderBy: orderAscending,
|
projectViewID: view.ID,
|
||||||
sortBy: taskPropertyKanbanPosition,
|
orderBy: orderAscending,
|
||||||
|
sortBy: taskPropertyPosition,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
opts.page = page
|
|
||||||
opts.perPage = perPage
|
|
||||||
opts.search = search
|
|
||||||
|
|
||||||
for _, filter := range opts.parsedFilters {
|
for _, filter := range opts.parsedFilters {
|
||||||
if filter.field == taskPropertyBucketID {
|
if filter.field == taskPropertyBucketID {
|
||||||
@ -192,11 +237,17 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||||||
for id, bucket := range bucketMap {
|
for id, bucket := range bucketMap {
|
||||||
|
|
||||||
if !strings.Contains(originalFilter, "bucket_id") {
|
if !strings.Contains(originalFilter, "bucket_id") {
|
||||||
|
|
||||||
|
var bucketFilter = "bucket_id = " + strconv.FormatInt(id, 10)
|
||||||
|
if view.BucketConfigurationMode == BucketConfigurationModeFilter {
|
||||||
|
bucketFilter = "(" + view.BucketConfiguration[id].Filter + ")"
|
||||||
|
}
|
||||||
|
|
||||||
var filterString string
|
var filterString string
|
||||||
if originalFilter == "" {
|
if originalFilter == "" {
|
||||||
filterString = "bucket_id = " + strconv.FormatInt(id, 10)
|
filterString = bucketFilter
|
||||||
} else {
|
} else {
|
||||||
filterString = "(" + originalFilter + ") && bucket_id = " + strconv.FormatInt(id, 10)
|
filterString = "(" + originalFilter + ") && " + bucketFilter
|
||||||
}
|
}
|
||||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString, opts.filterTimezone)
|
opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString, opts.filterTimezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -204,9 +255,13 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ts, _, total, err := getRawTasksForProjects(s, []*Project{{ID: bucket.ProjectID}}, auth, opts)
|
ts, _, total, err := getRawTasksForProjects(s, projects, auth, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range ts {
|
||||||
|
t.BucketID = bucket.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket.Count = total
|
bucket.Count = total
|
||||||
@ -219,9 +274,9 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||||||
taskMap[t.ID] = t
|
taskMap[t.ID] = t
|
||||||
}
|
}
|
||||||
|
|
||||||
err = addMoreInfoToTasks(s, taskMap, auth)
|
err = addMoreInfoToTasks(s, taskMap, auth, view)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put all tasks in their buckets
|
// Put all tasks in their buckets
|
||||||
@ -230,13 +285,13 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
// Check if the bucket exists in the map to prevent nil pointer panics
|
// Check if the bucket exists in the map to prevent nil pointer panics
|
||||||
if _, exists := bucketMap[task.BucketID]; !exists {
|
if _, exists := bucketMap[task.BucketID]; !exists {
|
||||||
log.Debugf("Tried to put task %d into bucket %d which does not exist in project %d", task.ID, task.BucketID, b.ProjectID)
|
log.Debugf("Tried to put task %d into bucket %d which does not exist in project %d", task.ID, task.BucketID, view.ProjectID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bucketMap[task.BucketID].Tasks = append(bucketMap[task.BucketID].Tasks, task)
|
bucketMap[task.BucketID].Tasks = append(bucketMap[task.BucketID].Tasks, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buckets, len(buckets), int64(len(buckets)), nil
|
return buckets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new bucket
|
// Create creates a new bucket
|
||||||
@ -247,12 +302,13 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security JWTKeyAuth
|
// @Security JWTKeyAuth
|
||||||
// @Param id path int true "Project Id"
|
// @Param id path int true "Project Id"
|
||||||
|
// @Param view path int true "Project view ID"
|
||||||
// @Param bucket body models.Bucket true "The bucket object"
|
// @Param bucket body models.Bucket true "The bucket object"
|
||||||
// @Success 200 {object} models.Bucket "The created bucket object."
|
// @Success 200 {object} models.Bucket "The created bucket object."
|
||||||
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
|
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
|
||||||
// @Failure 404 {object} web.HTTPError "The project does not exist."
|
// @Failure 404 {object} web.HTTPError "The project does not exist."
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /projects/{id}/buckets [put]
|
// @Router /projects/{id}/views/{view}/buckets [put]
|
||||||
func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||||
b.CreatedBy, err = GetUserOrLinkShareUser(s, a)
|
b.CreatedBy, err = GetUserOrLinkShareUser(s, a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -279,12 +335,13 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
// @Security JWTKeyAuth
|
// @Security JWTKeyAuth
|
||||||
// @Param projectID path int true "Project Id"
|
// @Param projectID path int true "Project Id"
|
||||||
// @Param bucketID path int true "Bucket Id"
|
// @Param bucketID path int true "Bucket Id"
|
||||||
|
// @Param view path int true "Project view ID"
|
||||||
// @Param bucket body models.Bucket true "The bucket object"
|
// @Param bucket body models.Bucket true "The bucket object"
|
||||||
// @Success 200 {object} models.Bucket "The created bucket object."
|
// @Success 200 {object} models.Bucket "The created bucket object."
|
||||||
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
|
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
|
||||||
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /projects/{projectID}/buckets/{bucketID} [post]
|
// @Router /projects/{projectID}/views/{view}/buckets/{bucketID} [post]
|
||||||
func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
|
func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||||
_, err = s.
|
_, err = s.
|
||||||
Where("id = ?", b.ID).
|
Where("id = ?", b.ID).
|
||||||
@ -292,6 +349,7 @@ func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
|
|||||||
"title",
|
"title",
|
||||||
"limit",
|
"limit",
|
||||||
"position",
|
"position",
|
||||||
|
"project_view_id",
|
||||||
).
|
).
|
||||||
Update(b)
|
Update(b)
|
||||||
return
|
return
|
||||||
@ -306,26 +364,27 @@ func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
|
|||||||
// @Security JWTKeyAuth
|
// @Security JWTKeyAuth
|
||||||
// @Param projectID path int true "Project Id"
|
// @Param projectID path int true "Project Id"
|
||||||
// @Param bucketID path int true "Bucket Id"
|
// @Param bucketID path int true "Bucket Id"
|
||||||
|
// @Param view path int true "Project view ID"
|
||||||
// @Success 200 {object} models.Message "Successfully deleted."
|
// @Success 200 {object} models.Message "Successfully deleted."
|
||||||
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /projects/{projectID}/buckets/{bucketID} [delete]
|
// @Router /projects/{projectID}/views/{view}/buckets/{bucketID} [delete]
|
||||||
func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
|
func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||||
|
|
||||||
// Prevent removing the last bucket
|
// Prevent removing the last bucket
|
||||||
total, err := s.Where("project_id = ?", b.ProjectID).Count(&Bucket{})
|
total, err := s.Where("project_view_id = ?", b.ProjectViewID).Count(&Bucket{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if total <= 1 {
|
if total <= 1 {
|
||||||
return ErrCannotRemoveLastBucket{
|
return ErrCannotRemoveLastBucket{
|
||||||
BucketID: b.ID,
|
BucketID: b.ID,
|
||||||
ProjectID: b.ProjectID,
|
ProjectViewID: b.ProjectViewID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the default bucket
|
// Get the default bucket
|
||||||
p, err := GetProjectSimpleByID(s, b.ProjectID)
|
p, err := GetProjectViewByIDAndProject(s, b.ProjectViewID, b.ProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -354,7 +413,7 @@ func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
_, err = s.
|
_, err = s.
|
||||||
Where("bucket_id = ?", b.ID).
|
Where("bucket_id = ?", b.ID).
|
||||||
Cols("bucket_id").
|
Cols("bucket_id").
|
||||||
Update(&Task{BucketID: defaultBucketID})
|
Update(&TaskBucket{BucketID: defaultBucketID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,11 @@ import (
|
|||||||
|
|
||||||
// CanCreate checks if a user can create a new bucket
|
// CanCreate checks if a user can create a new bucket
|
||||||
func (b *Bucket) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
func (b *Bucket) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||||
l := &Project{ID: b.ProjectID}
|
pv := &ProjectView{
|
||||||
return l.CanWrite(s, a)
|
ID: b.ProjectViewID,
|
||||||
|
ProjectID: b.ProjectID,
|
||||||
|
}
|
||||||
|
return pv.CanUpdate(s, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanUpdate checks if a user can update an existing bucket
|
// CanUpdate checks if a user can update an existing bucket
|
||||||
@ -43,6 +46,9 @@ func (b *Bucket) canDoBucket(s *xorm.Session, a web.Auth) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
l := &Project{ID: bb.ProjectID}
|
pv := &ProjectView{
|
||||||
return l.CanWrite(s, a)
|
ID: bb.ProjectViewID,
|
||||||
|
ProjectID: b.ProjectID,
|
||||||
|
}
|
||||||
|
return pv.CanUpdate(s, a)
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,10 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
testuser := &user.User{ID: 1}
|
testuser := &user.User{ID: 1}
|
||||||
b := &Bucket{ProjectID: 1}
|
b := &TaskCollection{
|
||||||
|
ProjectViewID: 4,
|
||||||
|
ProjectID: 1,
|
||||||
|
}
|
||||||
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
|
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -78,11 +81,10 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
testuser := &user.User{ID: 1}
|
testuser := &user.User{ID: 1}
|
||||||
b := &Bucket{
|
b := &TaskCollection{
|
||||||
ProjectID: 1,
|
ProjectViewID: 4,
|
||||||
TaskCollection: TaskCollection{
|
ProjectID: 1,
|
||||||
Filter: "title ~ 'done'",
|
Filter: "title ~ 'done'",
|
||||||
},
|
|
||||||
}
|
}
|
||||||
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
|
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -98,23 +100,19 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
testuser := &user.User{ID: 1}
|
testuser := &user.User{ID: 1}
|
||||||
b := &Bucket{
|
b := &TaskCollection{
|
||||||
ProjectID: 1,
|
ProjectViewID: 4,
|
||||||
TaskCollection: TaskCollection{
|
ProjectID: 1,
|
||||||
Filter: "title ~ 'task' && bucket_id = 2",
|
Filter: "title ~ 'task' && bucket_id = 2",
|
||||||
},
|
|
||||||
}
|
}
|
||||||
bucketsInterface, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
|
taskIn, _, _, err := b.ReadAll(s, testuser, "", -1, 0)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
buckets := bucketsInterface.([]*Bucket)
|
tasks := taskIn.([]*Task)
|
||||||
assert.Len(t, buckets, 3)
|
assert.Len(t, tasks, 3)
|
||||||
assert.Empty(t, buckets[0].Tasks, 0)
|
assert.Equal(t, int64(3), tasks[0].ID)
|
||||||
assert.Len(t, buckets[1].Tasks, 3)
|
assert.Equal(t, int64(4), tasks[1].ID)
|
||||||
assert.Empty(t, buckets[2].Tasks, 0)
|
assert.Equal(t, int64(5), tasks[2].ID)
|
||||||
assert.Equal(t, int64(3), buckets[1].Tasks[0].ID)
|
|
||||||
assert.Equal(t, int64(4), buckets[1].Tasks[1].ID)
|
|
||||||
assert.Equal(t, int64(5), buckets[1].Tasks[2].ID)
|
|
||||||
})
|
})
|
||||||
t.Run("accessed by link share", func(t *testing.T) {
|
t.Run("accessed by link share", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
@ -126,7 +124,10 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
Right: RightRead,
|
Right: RightRead,
|
||||||
}
|
}
|
||||||
b := &Bucket{ProjectID: 1}
|
b := &TaskCollection{
|
||||||
|
ProjectID: 1,
|
||||||
|
ProjectViewID: 4,
|
||||||
|
}
|
||||||
result, _, _, err := b.ReadAll(s, linkShare, "", 0, 0)
|
result, _, _, err := b.ReadAll(s, linkShare, "", 0, 0)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
buckets, _ := result.([]*Bucket)
|
buckets, _ := result.([]*Bucket)
|
||||||
@ -140,7 +141,10 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
testuser := &user.User{ID: 12}
|
testuser := &user.User{ID: 12}
|
||||||
b := &Bucket{ProjectID: 23}
|
b := &TaskCollection{
|
||||||
|
ProjectID: 23,
|
||||||
|
ProjectViewID: 92,
|
||||||
|
}
|
||||||
result, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
|
result, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
buckets, _ := result.([]*Bucket)
|
buckets, _ := result.([]*Bucket)
|
||||||
@ -151,7 +155,7 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBucket_Delete(t *testing.T) {
|
func TestBucket_Delete(t *testing.T) {
|
||||||
user := &user.User{ID: 1}
|
u := &user.User{ID: 1}
|
||||||
|
|
||||||
t.Run("normal", func(t *testing.T) {
|
t.Run("normal", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
@ -159,22 +163,23 @@ func TestBucket_Delete(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
b := &Bucket{
|
b := &Bucket{
|
||||||
ID: 2, // The second bucket only has 3 tasks
|
ID: 2, // The second bucket only has 3 tasks
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
|
ProjectViewID: 4,
|
||||||
}
|
}
|
||||||
err := b.Delete(s, user)
|
err := b.Delete(s, u)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = s.Commit()
|
err = s.Commit()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Assert all tasks have been moved to bucket 1 as that one is the first
|
// Assert all tasks have been moved to bucket 1 as that one is the first
|
||||||
tasks := []*Task{}
|
tasks := []*TaskBucket{}
|
||||||
err = s.Where("bucket_id = ?", 1).Find(&tasks)
|
err = s.Where("bucket_id = ?", 1).Find(&tasks)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, tasks, 15)
|
assert.Len(t, tasks, 15)
|
||||||
db.AssertMissing(t, "buckets", map[string]interface{}{
|
db.AssertMissing(t, "buckets", map[string]interface{}{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"project_id": 1,
|
"project_view_id": 4,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
t.Run("last bucket in project", func(t *testing.T) {
|
t.Run("last bucket in project", func(t *testing.T) {
|
||||||
@ -183,18 +188,19 @@ func TestBucket_Delete(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
b := &Bucket{
|
b := &Bucket{
|
||||||
ID: 34,
|
ID: 34,
|
||||||
ProjectID: 18,
|
ProjectID: 18,
|
||||||
|
ProjectViewID: 72,
|
||||||
}
|
}
|
||||||
err := b.Delete(s, user)
|
err := b.Delete(s, u)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.True(t, IsErrCannotRemoveLastBucket(err))
|
assert.True(t, IsErrCannotRemoveLastBucket(err))
|
||||||
err = s.Commit()
|
err = s.Commit()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
db.AssertExists(t, "buckets", map[string]interface{}{
|
db.AssertExists(t, "buckets", map[string]interface{}{
|
||||||
"id": 34,
|
"id": 34,
|
||||||
"project_id": 18,
|
"project_view_id": 72,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
t.Run("done bucket should be reset", func(t *testing.T) {
|
t.Run("done bucket should be reset", func(t *testing.T) {
|
||||||
@ -203,15 +209,16 @@ func TestBucket_Delete(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
b := &Bucket{
|
b := &Bucket{
|
||||||
ID: 3,
|
ID: 3,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
|
ProjectViewID: 4,
|
||||||
}
|
}
|
||||||
err := b.Delete(s, user)
|
err := b.Delete(s, u)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
db.AssertMissing(t, "projects", map[string]interface{}{
|
db.AssertMissing(t, "project_views", map[string]interface{}{
|
||||||
"id": 1,
|
"id": b.ProjectViewID,
|
||||||
"done_bucket_id": 3,
|
"done_bucket_id": 0,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -238,9 +245,10 @@ func TestBucket_Update(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
b := &Bucket{
|
b := &Bucket{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Title: "New Name",
|
Title: "New Name",
|
||||||
Limit: 2,
|
Limit: 2,
|
||||||
|
ProjectViewID: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
testAndAssertBucketUpdate(t, b, s)
|
testAndAssertBucketUpdate(t, b, s)
|
||||||
@ -251,9 +259,10 @@ func TestBucket_Update(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
b := &Bucket{
|
b := &Bucket{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Title: "testbucket1",
|
Title: "testbucket1",
|
||||||
Limit: 0,
|
Limit: 0,
|
||||||
|
ProjectViewID: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
testAndAssertBucketUpdate(t, b, s)
|
testAndAssertBucketUpdate(t, b, s)
|
||||||
|
@ -62,6 +62,9 @@ func GetTables() []interface{} {
|
|||||||
&TypesenseSync{},
|
&TypesenseSync{},
|
||||||
&Webhook{},
|
&Webhook{},
|
||||||
&Reaction{},
|
&Reaction{},
|
||||||
|
&ProjectView{},
|
||||||
|
&TaskPosition{},
|
||||||
|
&TaskBucket{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,9 +52,7 @@ type Project struct {
|
|||||||
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
|
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
|
||||||
ParentProject *Project `xorm:"-" json:"-"`
|
ParentProject *Project `xorm:"-" json:"-"`
|
||||||
|
|
||||||
// The ID of the bucket where new tasks without a bucket are added to. By default, this is the leftmost bucket in a project.
|
// Deprecated: If tasks are moved to the done bucket, they are marked as done. If they are marked as done individually, they are moved into the done bucket.
|
||||||
DefaultBucketID int64 `xorm:"bigint INDEX null" json:"default_bucket_id"`
|
|
||||||
// If tasks are moved to the done bucket, they are marked as done. If they are marked as done individually, they are moved into the done bucket.
|
|
||||||
DoneBucketID int64 `xorm:"bigint INDEX null" json:"done_bucket_id"`
|
DoneBucketID int64 `xorm:"bigint INDEX null" json:"done_bucket_id"`
|
||||||
|
|
||||||
// The user who created this project.
|
// The user who created this project.
|
||||||
@ -80,6 +78,8 @@ type Project struct {
|
|||||||
// The position this project has when querying all projects. See the tasks.position property on how to use this.
|
// The position this project has when querying all projects. See the tasks.position property on how to use this.
|
||||||
Position float64 `xorm:"double null" json:"position"`
|
Position float64 `xorm:"double null" json:"position"`
|
||||||
|
|
||||||
|
Views []*ProjectView `xorm:"-" json:"views"`
|
||||||
|
|
||||||
// A timestamp when this project was created. You cannot change this value.
|
// A timestamp when this project was created. You cannot change this value.
|
||||||
Created time.Time `xorm:"created not null" json:"created"`
|
Created time.Time `xorm:"created not null" json:"created"`
|
||||||
// A timestamp when this project was last updated. You cannot change this value.
|
// A timestamp when this project was last updated. You cannot change this value.
|
||||||
@ -266,6 +266,9 @@ func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = s.
|
||||||
|
Where("project_id = ?", p.ID).
|
||||||
|
Find(&p.Views)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -587,6 +590,23 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
|||||||
subscriptions = make(map[int64][]*Subscription)
|
subscriptions = make(map[int64][]*Subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views := []*ProjectView{}
|
||||||
|
err = s.
|
||||||
|
In("project_id", projectIDs).
|
||||||
|
Find(&views)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap := make(map[int64][]*ProjectView)
|
||||||
|
for _, v := range views {
|
||||||
|
if _, has := viewMap[v.ProjectID]; !has {
|
||||||
|
viewMap[v.ProjectID] = []*ProjectView{}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap[v.ProjectID] = append(viewMap[v.ProjectID], v)
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range projects {
|
for _, p := range projects {
|
||||||
if o, exists := owners[p.OwnerID]; exists {
|
if o, exists := owners[p.OwnerID]; exists {
|
||||||
p.Owner = o
|
p.Owner = o
|
||||||
@ -604,6 +624,11 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
|||||||
if subscription, exists := subscriptions[p.ID]; exists && len(subscription) > 0 {
|
if subscription, exists := subscriptions[p.ID]; exists && len(subscription) > 0 {
|
||||||
p.Subscription = subscription[0]
|
p.Subscription = subscription[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vs, has := viewMap[p.ID]
|
||||||
|
if has {
|
||||||
|
p.Views = vs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(fileIDs) == 0 {
|
if len(fileIDs) == 0 {
|
||||||
@ -713,7 +738,7 @@ func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) (err er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateProject(s *xorm.Session, project *Project, auth web.Auth, createBacklogBucket bool) (err error) {
|
func CreateProject(s *xorm.Session, project *Project, auth web.Auth, createBacklogBucket bool, createDefaultViews bool) (err error) {
|
||||||
err = project.CheckIsArchived(s)
|
err = project.CheckIsArchived(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -750,13 +775,8 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth, createBackl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if createBacklogBucket {
|
if createDefaultViews {
|
||||||
// Create a new first bucket for this project
|
err = CreateDefaultViewsForProject(s, project, auth, createBacklogBucket)
|
||||||
b := &Bucket{
|
|
||||||
ProjectID: project.ID,
|
|
||||||
Title: "Backlog",
|
|
||||||
}
|
|
||||||
err = b.Create(s, auth)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -969,7 +989,7 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
|
|||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /projects [put]
|
// @Router /projects [put]
|
||||||
func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
|
func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||||
err = CreateProject(s, p, a, true)
|
err = CreateProject(s, p, a, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,8 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||||||
pd.Project.ParentProjectID = pd.ParentProjectID
|
pd.Project.ParentProjectID = pd.ParentProjectID
|
||||||
// Set the owner to the current user
|
// Set the owner to the current user
|
||||||
pd.Project.OwnerID = doer.GetID()
|
pd.Project.OwnerID = doer.GetID()
|
||||||
if err := CreateProject(s, pd.Project, doer, false); err != nil {
|
err = CreateProject(s, pd.Project, doer, false, false)
|
||||||
|
if err != nil {
|
||||||
// If there is no available unique project identifier, just reset it.
|
// If there is no available unique project identifier, just reset it.
|
||||||
if IsErrProjectIdentifierIsNotUnique(err) {
|
if IsErrProjectIdentifierIsNotUnique(err) {
|
||||||
pd.Project.Identifier = ""
|
pd.Project.Identifier = ""
|
||||||
@ -92,32 +93,20 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||||||
|
|
||||||
log.Debugf("Duplicated project %d into new project %d", pd.ProjectID, pd.Project.ID)
|
log.Debugf("Duplicated project %d into new project %d", pd.ProjectID, pd.Project.ID)
|
||||||
|
|
||||||
// Duplicate kanban buckets
|
newTaskIDs, err := duplicateTasks(s, doer, pd)
|
||||||
// Old bucket ID as key, new id as value
|
|
||||||
// Used to map the newly created tasks to their new buckets
|
|
||||||
bucketMap := make(map[int64]int64)
|
|
||||||
buckets := []*Bucket{}
|
|
||||||
err = s.Where("project_id = ?", pd.ProjectID).Find(&buckets)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, b := range buckets {
|
|
||||||
oldID := b.ID
|
|
||||||
b.ID = 0
|
|
||||||
b.ProjectID = pd.Project.ID
|
|
||||||
if err := b.Create(s, doer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
bucketMap[oldID] = b.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Duplicated all buckets from project %d into %d", pd.ProjectID, pd.Project.ID)
|
log.Debugf("Duplicated all tasks from project %d into %d", pd.ProjectID, pd.Project.ID)
|
||||||
|
|
||||||
err = duplicateTasks(s, doer, pd, bucketMap)
|
err = duplicateViews(s, pd, doer, newTaskIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debugf("Duplicated all views, buckets and positions from project %d into %d", pd.ProjectID, pd.Project.ID)
|
||||||
|
|
||||||
err = duplicateProjectBackground(s, pd, doer)
|
err = duplicateProjectBackground(s, pd, doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -173,6 +162,94 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func duplicateViews(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth, taskMap map[int64]int64) (err error) {
|
||||||
|
// Duplicate Views
|
||||||
|
views := make(map[int64]*ProjectView)
|
||||||
|
err = s.Where("project_id = ?", pd.ProjectID).Find(&views)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldViewIDs := []int64{}
|
||||||
|
viewMap := make(map[int64]int64)
|
||||||
|
for _, view := range views {
|
||||||
|
oldID := view.ID
|
||||||
|
oldViewIDs = append(oldViewIDs, oldID)
|
||||||
|
|
||||||
|
view.ID = 0
|
||||||
|
view.ProjectID = pd.Project.ID
|
||||||
|
err = view.Create(s, doer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewMap[oldID] = view.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
buckets := []*Bucket{}
|
||||||
|
err = s.In("project_view_id", oldViewIDs).Find(&buckets)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old bucket ID as key, new id as value
|
||||||
|
// Used to map the newly created tasks to their new buckets
|
||||||
|
bucketMap := make(map[int64]int64)
|
||||||
|
|
||||||
|
oldBucketIDs := []int64{}
|
||||||
|
for _, b := range buckets {
|
||||||
|
oldID := b.ID
|
||||||
|
oldBucketIDs = append(oldBucketIDs, oldID)
|
||||||
|
|
||||||
|
b.ID = 0
|
||||||
|
b.ProjectID = pd.Project.ID
|
||||||
|
|
||||||
|
err = b.Create(s, doer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketMap[oldID] = b.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTaskBuckets := []*TaskBucket{}
|
||||||
|
err = s.In("bucket_id", oldBucketIDs).Find(&oldTaskBuckets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
taskBuckets := []*TaskBucket{}
|
||||||
|
for _, tb := range oldTaskBuckets {
|
||||||
|
taskBuckets = append(taskBuckets, &TaskBucket{
|
||||||
|
BucketID: bucketMap[tb.BucketID],
|
||||||
|
TaskID: taskMap[tb.TaskID],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.Insert(&taskBuckets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTaskPositions := []*TaskPosition{}
|
||||||
|
err = s.In("project_view_id", oldViewIDs).Find(&oldTaskPositions)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taskPositions := []*TaskPosition{}
|
||||||
|
for _, tp := range oldTaskPositions {
|
||||||
|
taskPositions = append(taskPositions, &TaskPosition{
|
||||||
|
ProjectViewID: viewMap[tp.ProjectViewID],
|
||||||
|
TaskID: taskMap[tp.TaskID],
|
||||||
|
Position: tp.Position,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.Insert(&taskPositions)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func duplicateProjectBackground(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth) (err error) {
|
func duplicateProjectBackground(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth) (err error) {
|
||||||
if pd.Project.BackgroundFileID == 0 {
|
if pd.Project.BackgroundFileID == 0 {
|
||||||
return
|
return
|
||||||
@ -221,33 +298,32 @@ func duplicateProjectBackground(s *xorm.Session, pd *ProjectDuplicate, doer web.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucketMap map[int64]int64) (err error) {
|
func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate) (newTaskIDs map[int64]int64, err error) {
|
||||||
// Get all tasks + all task details
|
// Get all tasks + all task details
|
||||||
tasks, _, _, err := getTasksForProjects(s, []*Project{{ID: ld.ProjectID}}, doer, &taskSearchOptions{})
|
tasks, _, _, err := getTasksForProjects(s, []*Project{{ID: ld.ProjectID}}, doer, &taskSearchOptions{}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tasks) == 0 {
|
if len(tasks) == 0 {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// This map contains the old task id as key and the new duplicated task id as value.
|
// This map contains the old task id as key and the new duplicated task id as value.
|
||||||
// It is used to map old task items to new ones.
|
// It is used to map old task items to new ones.
|
||||||
taskMap := make(map[int64]int64)
|
newTaskIDs = make(map[int64]int64, len(tasks))
|
||||||
// Create + update all tasks (includes reminders)
|
// Create + update all tasks (includes reminders)
|
||||||
oldTaskIDs := make([]int64, 0, len(tasks))
|
oldTaskIDs := make([]int64, 0, len(tasks))
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
oldID := t.ID
|
oldID := t.ID
|
||||||
t.ID = 0
|
t.ID = 0
|
||||||
t.ProjectID = ld.Project.ID
|
t.ProjectID = ld.Project.ID
|
||||||
t.BucketID = bucketMap[t.BucketID]
|
|
||||||
t.UID = ""
|
t.UID = ""
|
||||||
err := createTask(s, t, doer, false)
|
err = createTask(s, t, doer, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
taskMap[oldID] = t.ID
|
newTaskIDs[oldID] = t.ID
|
||||||
oldTaskIDs = append(oldTaskIDs, oldID)
|
oldTaskIDs = append(oldTaskIDs, oldID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,14 +334,14 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
|
|||||||
// file changes in the other project which is not something we want.
|
// file changes in the other project which is not something we want.
|
||||||
attachments, err := getTaskAttachmentsByTaskIDs(s, oldTaskIDs)
|
attachments, err := getTaskAttachmentsByTaskIDs(s, oldTaskIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, attachment := range attachments {
|
for _, attachment := range attachments {
|
||||||
oldAttachmentID := attachment.ID
|
oldAttachmentID := attachment.ID
|
||||||
attachment.ID = 0
|
attachment.ID = 0
|
||||||
var exists bool
|
var exists bool
|
||||||
attachment.TaskID, exists = taskMap[attachment.TaskID]
|
attachment.TaskID, exists = newTaskIDs[attachment.TaskID]
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Debugf("Error duplicating attachment %d from old task %d to new task: Old task <-> new task does not seem to exist.", oldAttachmentID, attachment.TaskID)
|
log.Debugf("Error duplicating attachment %d from old task %d to new task: Old task <-> new task does not seem to exist.", oldAttachmentID, attachment.TaskID)
|
||||||
continue
|
continue
|
||||||
@ -276,15 +352,15 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
|
|||||||
log.Debugf("Not duplicating attachment %d (file %d) because it does not exist from project %d into %d", oldAttachmentID, attachment.FileID, ld.ProjectID, ld.Project.ID)
|
log.Debugf("Not duplicating attachment %d (file %d) because it does not exist from project %d into %d", oldAttachmentID, attachment.FileID, ld.ProjectID, ld.Project.ID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := attachment.File.LoadFileByID(); err != nil {
|
if err := attachment.File.LoadFileByID(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, doer)
|
err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if attachment.File.File != nil {
|
if attachment.File.File != nil {
|
||||||
@ -305,9 +381,9 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
|
|||||||
|
|
||||||
for _, lt := range labelTasks {
|
for _, lt := range labelTasks {
|
||||||
lt.ID = 0
|
lt.ID = 0
|
||||||
lt.TaskID = taskMap[lt.TaskID]
|
lt.TaskID = newTaskIDs[lt.TaskID]
|
||||||
if _, err := s.Insert(lt); err != nil {
|
if _, err := s.Insert(lt); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,14 +398,14 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
|
|||||||
}
|
}
|
||||||
for _, a := range assignees {
|
for _, a := range assignees {
|
||||||
t := &Task{
|
t := &Task{
|
||||||
ID: taskMap[a.TaskID],
|
ID: newTaskIDs[a.TaskID],
|
||||||
ProjectID: ld.Project.ID,
|
ProjectID: ld.Project.ID,
|
||||||
}
|
}
|
||||||
if err := t.addNewAssigneeByID(s, a.UserID, ld.Project, doer); err != nil {
|
if err := t.addNewAssigneeByID(s, a.UserID, ld.Project, doer); err != nil {
|
||||||
if IsErrUserDoesNotHaveAccessToProject(err) {
|
if IsErrUserDoesNotHaveAccessToProject(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,9 +419,9 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
|
|||||||
}
|
}
|
||||||
for _, c := range comments {
|
for _, c := range comments {
|
||||||
c.ID = 0
|
c.ID = 0
|
||||||
c.TaskID = taskMap[c.TaskID]
|
c.TaskID = newTaskIDs[c.TaskID]
|
||||||
if _, err := s.Insert(c); err != nil {
|
if _, err := s.Insert(c); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,19 +436,19 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, r := range relations {
|
for _, r := range relations {
|
||||||
otherTaskID, exists := taskMap[r.OtherTaskID]
|
otherTaskID, exists := newTaskIDs[r.OtherTaskID]
|
||||||
if !exists {
|
if !exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
r.ID = 0
|
r.ID = 0
|
||||||
r.OtherTaskID = otherTaskID
|
r.OtherTaskID = otherTaskID
|
||||||
r.TaskID = taskMap[r.TaskID]
|
r.TaskID = newTaskIDs[r.TaskID]
|
||||||
if _, err := s.Insert(r); err != nil {
|
if _, err := s.Insert(r); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Duplicated all task relations from project %d into %d", ld.ProjectID, ld.Project.ID)
|
log.Debugf("Duplicated all task relations from project %d into %d", ld.ProjectID, ld.Project.ID)
|
||||||
|
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
@ -48,11 +48,11 @@ func TestProjectDuplicate(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// assert the new project has the same number of buckets as the old one
|
// assert the new project has the same number of buckets as the old one
|
||||||
numberOfOriginalBuckets, err := s.Where("project_id = ?", l.ProjectID).Count(&Bucket{})
|
numberOfOriginalViews, err := s.Where("project_id = ?", l.ProjectID).Count(&ProjectView{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
numberOfDuplicatedBuckets, err := s.Where("project_id = ?", l.Project.ID).Count(&Bucket{})
|
numberOfDuplicatedViews, err := s.Where("project_id = ?", l.Project.ID).Count(&ProjectView{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, numberOfOriginalBuckets, numberOfDuplicatedBuckets, "duplicated project does not have the same amount of buckets as the original one")
|
assert.Equal(t, numberOfOriginalViews, numberOfDuplicatedViews, "duplicated project does not have the same amount of views as the original one")
|
||||||
|
|
||||||
// To make this test 100% useful, it would need to assert a lot more stuff, but it is good enough for now.
|
// To make this test 100% useful, it would need to assert a lot more stuff, but it is good enough for now.
|
||||||
// Also, we're lacking utility functions to do all needed assertions.
|
// Also, we're lacking utility functions to do all needed assertions.
|
||||||
|
@ -118,6 +118,16 @@ func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fid := getSavedFilterIDFromProjectID(p.ID)
|
||||||
|
if fid > 0 {
|
||||||
|
sf, err := getSavedFilterSimpleByID(s, fid)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sf.CanUpdate(s, a)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the project
|
// Get the project
|
||||||
ol, err := GetProjectSimpleByID(s, p.ID)
|
ol, err := GetProjectSimpleByID(s, p.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -137,16 +147,6 @@ func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fid := getSavedFilterIDFromProjectID(p.ID)
|
|
||||||
if fid > 0 {
|
|
||||||
sf, err := getSavedFilterSimpleByID(s, fid)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return sf.CanUpdate(s, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
canUpdate, err = p.CanWrite(s, a)
|
canUpdate, err = p.CanWrite(s, a)
|
||||||
// If the project is archived and the user tries to un-archive it, let the request through
|
// If the project is archived and the user tries to un-archive it, let the request through
|
||||||
archivedErr := ErrProjectIsArchived{}
|
archivedErr := ErrProjectIsArchived{}
|
||||||
|
@ -53,8 +53,29 @@ func TestProject_CreateOrUpdate(t *testing.T) {
|
|||||||
"description": project.Description,
|
"description": project.Description,
|
||||||
"parent_project_id": 0,
|
"parent_project_id": 0,
|
||||||
}, false)
|
}, false)
|
||||||
db.AssertExists(t, "buckets", map[string]interface{}{
|
db.AssertExists(t, "project_views", map[string]interface{}{
|
||||||
"project_id": project.ID,
|
"project_id": project.ID,
|
||||||
|
"view_kind": ProjectViewKindList,
|
||||||
|
}, false)
|
||||||
|
db.AssertExists(t, "project_views", map[string]interface{}{
|
||||||
|
"project_id": project.ID,
|
||||||
|
"view_kind": ProjectViewKindGantt,
|
||||||
|
}, false)
|
||||||
|
db.AssertExists(t, "project_views", map[string]interface{}{
|
||||||
|
"project_id": project.ID,
|
||||||
|
"view_kind": ProjectViewKindTable,
|
||||||
|
}, false)
|
||||||
|
db.AssertExists(t, "project_views", map[string]interface{}{
|
||||||
|
"project_id": project.ID,
|
||||||
|
"view_kind": ProjectViewKindKanban,
|
||||||
|
"bucket_configuration_mode": BucketConfigurationModeManual,
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
kanbanView := &ProjectView{}
|
||||||
|
_, err = s.Where("project_id = ? AND view_kind = ?", project.ID, ProjectViewKindKanban).Get(kanbanView)
|
||||||
|
require.NoError(t, err)
|
||||||
|
db.AssertExists(t, "buckets", map[string]interface{}{
|
||||||
|
"project_view_id": kanbanView.ID,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
t.Run("nonexistant parent project", func(t *testing.T) {
|
t.Run("nonexistant parent project", func(t *testing.T) {
|
||||||
|
434
pkg/models/project_view.go
Normal file
434
pkg/models/project_view.go
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectViewKind int
|
||||||
|
|
||||||
|
func (p *ProjectViewKind) MarshalJSON() ([]byte, error) {
|
||||||
|
switch *p {
|
||||||
|
case ProjectViewKindList:
|
||||||
|
return []byte(`"list"`), nil
|
||||||
|
case ProjectViewKindGantt:
|
||||||
|
return []byte(`"gantt"`), nil
|
||||||
|
case ProjectViewKindTable:
|
||||||
|
return []byte(`"table"`), nil
|
||||||
|
case ProjectViewKindKanban:
|
||||||
|
return []byte(`"kanban"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(`null`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectViewKind) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var value string
|
||||||
|
err := json.Unmarshal(bytes, &value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value {
|
||||||
|
case "list":
|
||||||
|
*p = ProjectViewKindList
|
||||||
|
case "gantt":
|
||||||
|
*p = ProjectViewKindGantt
|
||||||
|
case "table":
|
||||||
|
*p = ProjectViewKindTable
|
||||||
|
case "kanban":
|
||||||
|
*p = ProjectViewKindKanban
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown project view kind: %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProjectViewKindList ProjectViewKind = iota
|
||||||
|
ProjectViewKindGantt
|
||||||
|
ProjectViewKindTable
|
||||||
|
ProjectViewKindKanban
|
||||||
|
)
|
||||||
|
|
||||||
|
type BucketConfigurationModeKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
BucketConfigurationModeNone BucketConfigurationModeKind = iota
|
||||||
|
BucketConfigurationModeManual
|
||||||
|
BucketConfigurationModeFilter
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *BucketConfigurationModeKind) MarshalJSON() ([]byte, error) {
|
||||||
|
switch *p {
|
||||||
|
case BucketConfigurationModeNone:
|
||||||
|
return []byte(`"none"`), nil
|
||||||
|
case BucketConfigurationModeManual:
|
||||||
|
return []byte(`"manual"`), nil
|
||||||
|
case BucketConfigurationModeFilter:
|
||||||
|
return []byte(`"filter"`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(`null`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BucketConfigurationModeKind) UnmarshalJSON(bytes []byte) error {
|
||||||
|
var value string
|
||||||
|
err := json.Unmarshal(bytes, &value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value {
|
||||||
|
case "none":
|
||||||
|
*p = BucketConfigurationModeNone
|
||||||
|
case "manual":
|
||||||
|
*p = BucketConfigurationModeManual
|
||||||
|
case "filter":
|
||||||
|
*p = BucketConfigurationModeFilter
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown bucket configuration mode kind: %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectViewBucketConfiguration struct {
|
||||||
|
Title string
|
||||||
|
Filter string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectView struct {
|
||||||
|
// The unique numeric id of this view
|
||||||
|
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||||
|
// The title of this view
|
||||||
|
Title string `xorm:"varchar(255) not null" json:"title" valid:"required,runelength(1|250)"`
|
||||||
|
// The project this view belongs to
|
||||||
|
ProjectID int64 `xorm:"not null index" json:"project_id" param:"project"`
|
||||||
|
// The kind of this view. Can be `list`, `gantt`, `table` or `kanban`.
|
||||||
|
ViewKind ProjectViewKind `xorm:"not null" json:"view_kind"`
|
||||||
|
|
||||||
|
// The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation.
|
||||||
|
Filter string `xorm:"text null default null" query:"filter" json:"filter"`
|
||||||
|
// The position of this view in the list. The list of all views will be sorted by this parameter.
|
||||||
|
Position float64 `xorm:"double null" json:"position"`
|
||||||
|
|
||||||
|
// The bucket configuration mode. Can be `none`, `manual` or `filter`. `manual` allows to move tasks between buckets as you normally would. `filter` creates buckets based on a filter for each bucket.
|
||||||
|
BucketConfigurationMode BucketConfigurationModeKind `xorm:"default 0" json:"bucket_configuration_mode"`
|
||||||
|
// When the bucket configuration mode is not `manual`, this field holds the options of that configuration.
|
||||||
|
BucketConfiguration []*ProjectViewBucketConfiguration `xorm:"json" json:"bucket_configuration"`
|
||||||
|
// The ID of the bucket where new tasks without a bucket are added to. By default, this is the leftmost bucket in a view.
|
||||||
|
DefaultBucketID int64 `xorm:"bigint INDEX null" json:"default_bucket_id"`
|
||||||
|
// If tasks are moved to the done bucket, they are marked as done. If they are marked as done individually, they are moved into the done bucket.
|
||||||
|
DoneBucketID int64 `xorm:"bigint INDEX null" json:"done_bucket_id"`
|
||||||
|
|
||||||
|
// A timestamp when this view was updated. You cannot change this value.
|
||||||
|
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||||
|
// A timestamp when this reaction was created. You cannot change this value.
|
||||||
|
Created time.Time `xorm:"created not null" json:"created"`
|
||||||
|
|
||||||
|
web.CRUDable `xorm:"-" json:"-"`
|
||||||
|
web.Rights `xorm:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectView) TableName() string {
|
||||||
|
return "project_views"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getViewsForProject(s *xorm.Session, projectID int64) (views []*ProjectView, err error) {
|
||||||
|
views = []*ProjectView{}
|
||||||
|
err = s.
|
||||||
|
Where("project_id = ?", projectID).
|
||||||
|
Find(&views)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAll gets all project views
|
||||||
|
// @Summary Get all project views for a project
|
||||||
|
// @Description Returns all project views for a sepcific project
|
||||||
|
// @tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWTKeyAuth
|
||||||
|
// @Param project path int true "Project ID"
|
||||||
|
// @Success 200 {array} models.ProjectView "The project views"
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /projects/{project}/views [get]
|
||||||
|
func (p *ProjectView) ReadAll(s *xorm.Session, a web.Auth, _ string, _ int, _ int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||||
|
|
||||||
|
pp := &Project{ID: p.ProjectID}
|
||||||
|
can, _, err := pp.CanRead(s, a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, err
|
||||||
|
}
|
||||||
|
if !can {
|
||||||
|
return nil, 0, 0, ErrGenericForbidden{}
|
||||||
|
}
|
||||||
|
|
||||||
|
projectViews, err := getViewsForProject(s, p.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCount, err := s.
|
||||||
|
Where("project_id = ?", p.ProjectID).
|
||||||
|
Count(&ProjectView{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectViews, len(projectViews), totalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadOne implements the CRUD method to get one project view
|
||||||
|
// @Summary Get one project view
|
||||||
|
// @Description Returns a project view by its ID.
|
||||||
|
// @tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWTKeyAuth
|
||||||
|
// @Param project path int true "Project ID"
|
||||||
|
// @Param id path int true "Project View ID"
|
||||||
|
// @Success 200 {object} models.ProjectView "The project view"
|
||||||
|
// @Failure 403 {object} web.HTTPError "The user does not have access to this project view"
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /projects/{project}/views/{id} [get]
|
||||||
|
func (p *ProjectView) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
|
||||||
|
view, err := GetProjectViewByIDAndProject(s, p.ID, p.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = *view
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the project view
|
||||||
|
// @Summary Delete a project view
|
||||||
|
// @Description Deletes a project view.
|
||||||
|
// @tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWTKeyAuth
|
||||||
|
// @Param project path int true "Project ID"
|
||||||
|
// @Param id path int true "Project View ID"
|
||||||
|
// @Success 200 {object} models.Message "The project view was successfully deleted."
|
||||||
|
// @Failure 403 {object} web.HTTPError "The user does not have access to the project view"
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /projects/{project}/views/{id} [delete]
|
||||||
|
func (p *ProjectView) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||||
|
_, err = s.
|
||||||
|
Where("id = ? AND project_id = ?", p.ID, p.ProjectID).
|
||||||
|
Delete(&ProjectView{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create adds a new project view
|
||||||
|
// @Summary Create a project view
|
||||||
|
// @Description Create a project view in a specific project.
|
||||||
|
// @tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWTKeyAuth
|
||||||
|
// @Param project path int true "Project ID"
|
||||||
|
// @Param view body models.ProjectView true "The project view you want to create."
|
||||||
|
// @Success 200 {object} models.ProjectView "The created project view"
|
||||||
|
// @Failure 403 {object} web.HTTPError "The user does not have access to create a project view"
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /projects/{project}/views [put]
|
||||||
|
func (p *ProjectView) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||||
|
return createProjectView(s, p, a, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createProjectView(s *xorm.Session, p *ProjectView, a web.Auth, createBacklogBucket bool) (err error) {
|
||||||
|
_, err = s.Insert(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if createBacklogBucket && p.BucketConfigurationMode == BucketConfigurationModeManual {
|
||||||
|
// Create a new first bucket for this project
|
||||||
|
b := &Bucket{
|
||||||
|
ProjectViewID: p.ID,
|
||||||
|
Title: "Backlog",
|
||||||
|
}
|
||||||
|
err = b.Create(s, a)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move all tasks into the new bucket when the project already has tasks
|
||||||
|
c := &TaskCollection{
|
||||||
|
ProjectID: p.ProjectID,
|
||||||
|
}
|
||||||
|
ts, _, _, err := c.ReadAll(s, a, "", 0, -1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tasks := ts.([]*Task)
|
||||||
|
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
taskBuckets := []*TaskBucket{}
|
||||||
|
for _, task := range tasks {
|
||||||
|
taskBuckets = append(taskBuckets, &TaskBucket{
|
||||||
|
TaskID: task.ID,
|
||||||
|
BucketID: b.ID,
|
||||||
|
ProjectViewID: p.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.Insert(&taskBuckets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RecalculateTaskPositions(s, p, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update is the handler to update a project view
|
||||||
|
// @Summary Updates a project view
|
||||||
|
// @Description Updates a project view.
|
||||||
|
// @tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWTKeyAuth
|
||||||
|
// @Param project path int true "Project ID"
|
||||||
|
// @Param id path int true "Project View ID"
|
||||||
|
// @Param view body models.ProjectView true "The project view with updated values you want to change."
|
||||||
|
// @Success 200 {object} models.ProjectView "The updated project view."
|
||||||
|
// @Failure 400 {object} web.HTTPError "Invalid project view object provided."
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /projects/{project}/views/{id} [post]
|
||||||
|
func (p *ProjectView) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||||
|
// Check if the project view exists
|
||||||
|
_, err = GetProjectViewByIDAndProject(s, p.ID, p.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.ID(p.ID).Update(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetProjectViewByIDAndProject(s *xorm.Session, id, projectID int64) (view *ProjectView, err error) {
|
||||||
|
view = &ProjectView{}
|
||||||
|
exists, err := s.
|
||||||
|
Where("id = ? AND project_id = ?", id, projectID).
|
||||||
|
NoAutoCondition().
|
||||||
|
Get(view)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, &ErrProjectViewDoesNotExist{
|
||||||
|
ProjectViewID: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetProjectViewByID(s *xorm.Session, id int64) (view *ProjectView, err error) {
|
||||||
|
view = &ProjectView{}
|
||||||
|
exists, err := s.
|
||||||
|
Where("id = ?", id).
|
||||||
|
NoAutoCondition().
|
||||||
|
Get(view)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, &ErrProjectViewDoesNotExist{
|
||||||
|
ProjectViewID: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateDefaultViewsForProject(s *xorm.Session, project *Project, a web.Auth, createBacklogBucket bool) (err error) {
|
||||||
|
list := &ProjectView{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
Title: "List",
|
||||||
|
ViewKind: ProjectViewKindList,
|
||||||
|
Position: 100,
|
||||||
|
}
|
||||||
|
err = createProjectView(s, list, a, createBacklogBucket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gantt := &ProjectView{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
Title: "Gantt",
|
||||||
|
ViewKind: ProjectViewKindGantt,
|
||||||
|
Position: 200,
|
||||||
|
}
|
||||||
|
err = createProjectView(s, gantt, a, createBacklogBucket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table := &ProjectView{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
Title: "Table",
|
||||||
|
ViewKind: ProjectViewKindTable,
|
||||||
|
Position: 300,
|
||||||
|
}
|
||||||
|
err = createProjectView(s, table, a, createBacklogBucket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
kanban := &ProjectView{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
Title: "Kanban",
|
||||||
|
ViewKind: ProjectViewKindKanban,
|
||||||
|
Position: 400,
|
||||||
|
BucketConfigurationMode: BucketConfigurationModeManual,
|
||||||
|
}
|
||||||
|
err = createProjectView(s, kanban, a, createBacklogBucket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
project.Views = []*ProjectView{
|
||||||
|
list,
|
||||||
|
gantt,
|
||||||
|
table,
|
||||||
|
kanban,
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
46
pkg/models/project_view_rights.go
Normal file
46
pkg/models/project_view_rights.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *ProjectView) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||||
|
pp := p.getProject()
|
||||||
|
return pp.CanRead(s, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectView) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||||
|
pp := p.getProject()
|
||||||
|
return pp.CanUpdate(s, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||||
|
pp := p.getProject()
|
||||||
|
return pp.CanUpdate(s, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectView) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||||
|
pp := p.getProject()
|
||||||
|
return pp.CanUpdate(s, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProjectView) getProject() (pp *Project) {
|
||||||
|
return &Project{ID: p.ProjectID}
|
||||||
|
}
|
@ -116,9 +116,14 @@ func (sf *SavedFilter) toProject() *Project {
|
|||||||
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /filters [put]
|
// @Router /filters [put]
|
||||||
func (sf *SavedFilter) Create(s *xorm.Session, auth web.Auth) error {
|
func (sf *SavedFilter) Create(s *xorm.Session, auth web.Auth) (err error) {
|
||||||
sf.OwnerID = auth.GetID()
|
sf.OwnerID = auth.GetID()
|
||||||
_, err := s.Insert(sf)
|
_, err = s.Insert(sf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = CreateDefaultViewsForProject(s, &Project{ID: getProjectIDFromSavedFilterID(sf.ID)}, auth, true)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,14 +17,18 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/user"
|
"code.vikunja.io/api/pkg/user"
|
||||||
"code.vikunja.io/web"
|
"code.vikunja.io/web"
|
||||||
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TaskCollection is a struct used to hold filter details and not clutter the Task struct with information not related to actual tasks.
|
// TaskCollection is a struct used to hold filter details and not clutter the Task struct with information not related to actual tasks.
|
||||||
type TaskCollection struct {
|
type TaskCollection struct {
|
||||||
ProjectID int64 `param:"project" json:"-"`
|
ProjectID int64 `param:"project" json:"-"`
|
||||||
|
ProjectViewID int64 `param:"view" json:"-"`
|
||||||
|
|
||||||
// The query parameter to sort by. This is for ex. done, priority, etc.
|
// The query parameter to sort by. This is for ex. done, priority, etc.
|
||||||
SortBy []string `query:"sort_by" json:"sort_by"`
|
SortBy []string `query:"sort_by" json:"sort_by"`
|
||||||
@ -33,7 +37,7 @@ type TaskCollection struct {
|
|||||||
OrderBy []string `query:"order_by" json:"order_by"`
|
OrderBy []string `query:"order_by" json:"order_by"`
|
||||||
OrderByArr []string `query:"order_by[]" json:"-"`
|
OrderByArr []string `query:"order_by[]" json:"-"`
|
||||||
|
|
||||||
// The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature.
|
// The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation.
|
||||||
Filter string `query:"filter" json:"filter"`
|
Filter string `query:"filter" json:"filter"`
|
||||||
// The time zone which should be used for date match (statements like "now" resolve to different actual times)
|
// The time zone which should be used for date match (statements like "now" resolve to different actual times)
|
||||||
FilterTimezone string `query:"filter_timezone" json:"-"`
|
FilterTimezone string `query:"filter_timezone" json:"-"`
|
||||||
@ -41,6 +45,8 @@ type TaskCollection struct {
|
|||||||
// If set to true, the result will also include null values
|
// If set to true, the result will also include null values
|
||||||
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
|
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
|
||||||
|
|
||||||
|
isSavedFilter bool
|
||||||
|
|
||||||
web.CRUDable `xorm:"-" json:"-"`
|
web.CRUDable `xorm:"-" json:"-"`
|
||||||
web.Rights `xorm:"-" json:"-"`
|
web.Rights `xorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
@ -66,7 +72,6 @@ func validateTaskField(fieldName string) error {
|
|||||||
taskPropertyCreated,
|
taskPropertyCreated,
|
||||||
taskPropertyUpdated,
|
taskPropertyUpdated,
|
||||||
taskPropertyPosition,
|
taskPropertyPosition,
|
||||||
taskPropertyKanbanPosition,
|
|
||||||
taskPropertyBucketID,
|
taskPropertyBucketID,
|
||||||
taskPropertyIndex:
|
taskPropertyIndex:
|
||||||
return nil
|
return nil
|
||||||
@ -74,7 +79,7 @@ func validateTaskField(fieldName string) error {
|
|||||||
return ErrInvalidTaskField{TaskField: fieldName}
|
return ErrInvalidTaskField{TaskField: fieldName}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOptions, err error) {
|
func getTaskFilterOptsFromCollection(tf *TaskCollection, projectView *ProjectView) (opts *taskSearchOptions, err error) {
|
||||||
if len(tf.SortByArr) > 0 {
|
if len(tf.SortByArr) > 0 {
|
||||||
tf.SortBy = append(tf.SortBy, tf.SortByArr...)
|
tf.SortBy = append(tf.SortBy, tf.SortByArr...)
|
||||||
}
|
}
|
||||||
@ -95,6 +100,10 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
|||||||
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
|
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s == taskPropertyPosition && projectView != nil {
|
||||||
|
param.projectViewID = projectView.ID
|
||||||
|
}
|
||||||
|
|
||||||
// Param validation
|
// Param validation
|
||||||
if err := param.validate(); err != nil {
|
if err := param.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -113,6 +122,45 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
|||||||
return opts, err
|
return opts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTaskOrTasksInBuckets(s *xorm.Session, a web.Auth, projects []*Project, view *ProjectView, opts *taskSearchOptions) (tasks interface{}, resultCount int, totalItems int64, err error) {
|
||||||
|
if view != nil && !strings.Contains(opts.filter, "bucket_id") {
|
||||||
|
if view.BucketConfigurationMode != BucketConfigurationModeNone {
|
||||||
|
tasksInBuckets, err := GetTasksInBucketsForView(s, view, projects, opts, a)
|
||||||
|
return tasksInBuckets, len(tasksInBuckets), int64(len(tasksInBuckets)), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getTasksForProjects(s, projects, a, opts, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRelevantProjectsFromCollection(s *xorm.Session, a web.Auth, tf *TaskCollection) (projects []*Project, err error) {
|
||||||
|
if tf.ProjectID == 0 || tf.isSavedFilter {
|
||||||
|
projects, _, _, err = getRawProjectsForUser(
|
||||||
|
s,
|
||||||
|
&projectOptions{
|
||||||
|
user: &user.User{ID: a.GetID()},
|
||||||
|
page: -1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return projects, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the project exists and the user has access on it
|
||||||
|
project := &Project{ID: tf.ProjectID}
|
||||||
|
canRead, _, err := project.CanRead(s, a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !canRead {
|
||||||
|
return nil, ErrUserDoesNotHaveAccessToProject{
|
||||||
|
ProjectID: tf.ProjectID,
|
||||||
|
UserID: a.GetID(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*Project{{ID: tf.ProjectID}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReadAll gets all tasks for a collection
|
// ReadAll gets all tasks for a collection
|
||||||
// @Summary Get tasks in a project
|
// @Summary Get tasks in a project
|
||||||
// @Description Returns all tasks for the current project.
|
// @Description Returns all tasks for the current project.
|
||||||
@ -120,6 +168,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "The project ID."
|
// @Param id path int true "The project ID."
|
||||||
|
// @Param view path int true "The project view ID."
|
||||||
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||||
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
||||||
// @Param s query string false "Search tasks by task text."
|
// @Param s query string false "Search tasks by task text."
|
||||||
@ -131,12 +180,12 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
|
|||||||
// @Security JWTKeyAuth
|
// @Security JWTKeyAuth
|
||||||
// @Success 200 {array} models.Task "The tasks"
|
// @Success 200 {array} models.Task "The tasks"
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /projects/{id}/tasks [get]
|
// @Router /projects/{id}/views/{view}/tasks [get]
|
||||||
func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||||
|
|
||||||
// If the project id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter
|
// If the project id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter
|
||||||
// -1 is the favorites project which works as intended
|
// -1 is the favorites project which works as intended
|
||||||
if tf.ProjectID < -1 {
|
if !tf.isSavedFilter && tf.ProjectID < -1 {
|
||||||
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(tf.ProjectID))
|
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(tf.ProjectID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
@ -166,17 +215,46 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||||||
sf.Filters.FilterTimezone = u.Timezone
|
sf.Filters.FilterTimezone = u.Timezone
|
||||||
}
|
}
|
||||||
|
|
||||||
return sf.getTaskCollection().ReadAll(s, a, search, page, perPage)
|
tc := sf.getTaskCollection()
|
||||||
|
tc.ProjectViewID = tf.ProjectViewID
|
||||||
|
tc.ProjectID = tf.ProjectID
|
||||||
|
tc.isSavedFilter = true
|
||||||
|
|
||||||
|
return tc.ReadAll(s, a, search, page, perPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
taskopts, err := getTaskFilterOptsFromCollection(tf)
|
var view *ProjectView
|
||||||
|
if tf.ProjectViewID != 0 {
|
||||||
|
view, err = GetProjectViewByIDAndProject(s, tf.ProjectViewID, tf.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.Filter != "" {
|
||||||
|
if tf.Filter != "" {
|
||||||
|
tf.Filter = "(" + tf.Filter + ") && (" + view.Filter + ")"
|
||||||
|
} else {
|
||||||
|
tf.Filter = view.Filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getTaskFilterOptsFromCollection(tf, view)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
taskopts.search = search
|
opts.search = search
|
||||||
taskopts.page = page
|
opts.page = page
|
||||||
taskopts.perPage = perPage
|
opts.perPage = perPage
|
||||||
|
|
||||||
|
if view != nil {
|
||||||
|
opts.sortby = append(opts.sortby, &sortParam{
|
||||||
|
projectViewID: view.ID,
|
||||||
|
sortBy: taskPropertyPosition,
|
||||||
|
orderBy: orderAscending,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
shareAuth, is := a.(*LinkSharing)
|
shareAuth, is := a.(*LinkSharing)
|
||||||
if is {
|
if is {
|
||||||
@ -184,38 +262,13 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
return getTasksForProjects(s, []*Project{project}, a, taskopts)
|
return getTaskOrTasksInBuckets(s, a, []*Project{project}, view, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the project ID is not set, we get all tasks for the user.
|
projects, err := getRelevantProjectsFromCollection(s, a, tf)
|
||||||
// This allows to use this function in Task.ReadAll with a possibility to deprecate the latter at some point.
|
if err != nil {
|
||||||
var projects []*Project
|
return nil, 0, 0, err
|
||||||
if tf.ProjectID == 0 {
|
|
||||||
projects, _, _, err = getRawProjectsForUser(
|
|
||||||
s,
|
|
||||||
&projectOptions{
|
|
||||||
user: &user.User{ID: a.GetID()},
|
|
||||||
page: -1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check the project exists and the user has access on it
|
|
||||||
project := &Project{ID: tf.ProjectID}
|
|
||||||
canRead, _, err := project.CanRead(s, a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
if !canRead {
|
|
||||||
return nil, 0, 0, ErrUserDoesNotHaveAccessToProject{
|
|
||||||
ProjectID: tf.ProjectID,
|
|
||||||
UserID: a.GetID(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
projects = []*Project{{ID: tf.ProjectID}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTasksForProjects(s, projects, a, taskopts)
|
return getTaskOrTasksInBuckets(s, a, projects, view, opts)
|
||||||
}
|
}
|
||||||
|
@ -18,35 +18,36 @@ package models
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
sortParam struct {
|
sortParam struct {
|
||||||
sortBy string
|
sortBy string
|
||||||
orderBy sortOrder // asc or desc
|
orderBy sortOrder // asc or desc
|
||||||
|
projectViewID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
sortOrder string
|
sortOrder string
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
taskPropertyID string = "id"
|
taskPropertyID string = "id"
|
||||||
taskPropertyTitle string = "title"
|
taskPropertyTitle string = "title"
|
||||||
taskPropertyDescription string = "description"
|
taskPropertyDescription string = "description"
|
||||||
taskPropertyDone string = "done"
|
taskPropertyDone string = "done"
|
||||||
taskPropertyDoneAt string = "done_at"
|
taskPropertyDoneAt string = "done_at"
|
||||||
taskPropertyDueDate string = "due_date"
|
taskPropertyDueDate string = "due_date"
|
||||||
taskPropertyCreatedByID string = "created_by_id"
|
taskPropertyCreatedByID string = "created_by_id"
|
||||||
taskPropertyProjectID string = "project_id"
|
taskPropertyProjectID string = "project_id"
|
||||||
taskPropertyRepeatAfter string = "repeat_after"
|
taskPropertyRepeatAfter string = "repeat_after"
|
||||||
taskPropertyPriority string = "priority"
|
taskPropertyPriority string = "priority"
|
||||||
taskPropertyStartDate string = "start_date"
|
taskPropertyStartDate string = "start_date"
|
||||||
taskPropertyEndDate string = "end_date"
|
taskPropertyEndDate string = "end_date"
|
||||||
taskPropertyHexColor string = "hex_color"
|
taskPropertyHexColor string = "hex_color"
|
||||||
taskPropertyPercentDone string = "percent_done"
|
taskPropertyPercentDone string = "percent_done"
|
||||||
taskPropertyUID string = "uid"
|
taskPropertyUID string = "uid"
|
||||||
taskPropertyCreated string = "created"
|
taskPropertyCreated string = "created"
|
||||||
taskPropertyUpdated string = "updated"
|
taskPropertyUpdated string = "updated"
|
||||||
taskPropertyPosition string = "position"
|
taskPropertyPosition string = "position"
|
||||||
taskPropertyKanbanPosition string = "kanban_position"
|
taskPropertyBucketID string = "bucket_id"
|
||||||
taskPropertyBucketID string = "bucket_id"
|
taskPropertyIndex string = "index"
|
||||||
taskPropertyIndex string = "index"
|
taskPropertyProjectViewID string = "project_view_id"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -73,5 +74,10 @@ func (sp *sortParam) validate() error {
|
|||||||
if sp.orderBy != orderDescending && sp.orderBy != orderAscending {
|
if sp.orderBy != orderDescending && sp.orderBy != orderAscending {
|
||||||
return ErrInvalidSortOrder{OrderBy: sp.orderBy}
|
return ErrInvalidSortOrder{OrderBy: sp.orderBy}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sp.sortBy == taskPropertyPosition && sp.projectViewID == 0 {
|
||||||
|
return ErrMustHaveProjectViewToSortByPosition{}
|
||||||
|
}
|
||||||
|
|
||||||
return validateTaskField(sp.sortBy)
|
return validateTaskField(sp.sortBy)
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,6 @@ func TestSortParamValidation(t *testing.T) {
|
|||||||
taskPropertyUID,
|
taskPropertyUID,
|
||||||
taskPropertyCreated,
|
taskPropertyCreated,
|
||||||
taskPropertyUpdated,
|
taskPropertyUpdated,
|
||||||
taskPropertyPosition,
|
|
||||||
} {
|
} {
|
||||||
t.Run(test, func(t *testing.T) {
|
t.Run(test, func(t *testing.T) {
|
||||||
s := &sortParam{
|
s := &sortParam{
|
||||||
|
@ -95,9 +95,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedByID: 1,
|
CreatedByID: 1,
|
||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
BucketID: 1,
|
|
||||||
IsFavorite: true,
|
IsFavorite: true,
|
||||||
Position: 2,
|
|
||||||
Reactions: ReactionMap{
|
Reactions: ReactionMap{
|
||||||
"👋": []*user.User{user1},
|
"👋": []*user.User{user1},
|
||||||
},
|
},
|
||||||
@ -112,7 +110,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
Index: 14,
|
Index: 14,
|
||||||
CreatedByID: 1,
|
CreatedByID: 1,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
},
|
},
|
||||||
@ -170,8 +167,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedByID: 1,
|
CreatedByID: 1,
|
||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
BucketID: 1,
|
|
||||||
Position: 4,
|
|
||||||
Labels: []*Label{
|
Labels: []*Label{
|
||||||
label4,
|
label4,
|
||||||
},
|
},
|
||||||
@ -199,7 +194,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
Priority: 100,
|
Priority: 100,
|
||||||
BucketID: 2,
|
|
||||||
}
|
}
|
||||||
task4 := &Task{
|
task4 := &Task{
|
||||||
ID: 4,
|
ID: 4,
|
||||||
@ -213,7 +207,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
Priority: 1,
|
Priority: 1,
|
||||||
BucketID: 2,
|
|
||||||
}
|
}
|
||||||
task5 := &Task{
|
task5 := &Task{
|
||||||
ID: 5,
|
ID: 5,
|
||||||
@ -227,7 +220,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
DueDate: time.Unix(1543636724, 0).In(loc),
|
DueDate: time.Unix(1543636724, 0).In(loc),
|
||||||
BucketID: 2,
|
|
||||||
}
|
}
|
||||||
task6 := &Task{
|
task6 := &Task{
|
||||||
ID: 6,
|
ID: 6,
|
||||||
@ -241,7 +233,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
DueDate: time.Unix(1543616724, 0).In(loc),
|
DueDate: time.Unix(1543616724, 0).In(loc),
|
||||||
BucketID: 3,
|
|
||||||
}
|
}
|
||||||
task7 := &Task{
|
task7 := &Task{
|
||||||
ID: 7,
|
ID: 7,
|
||||||
@ -255,7 +246,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
StartDate: time.Unix(1544600000, 0).In(loc),
|
StartDate: time.Unix(1544600000, 0).In(loc),
|
||||||
BucketID: 3,
|
|
||||||
}
|
}
|
||||||
task8 := &Task{
|
task8 := &Task{
|
||||||
ID: 8,
|
ID: 8,
|
||||||
@ -269,7 +259,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
EndDate: time.Unix(1544700000, 0).In(loc),
|
EndDate: time.Unix(1544700000, 0).In(loc),
|
||||||
BucketID: 3,
|
|
||||||
}
|
}
|
||||||
task9 := &Task{
|
task9 := &Task{
|
||||||
ID: 9,
|
ID: 9,
|
||||||
@ -280,7 +269,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
StartDate: time.Unix(1544600000, 0).In(loc),
|
StartDate: time.Unix(1544600000, 0).In(loc),
|
||||||
@ -295,7 +283,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -308,7 +295,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -321,7 +307,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -335,7 +320,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
ProjectID: 6,
|
ProjectID: 6,
|
||||||
IsFavorite: true,
|
IsFavorite: true,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 6,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -348,7 +332,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 7,
|
ProjectID: 7,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 7,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -361,7 +344,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 8,
|
ProjectID: 8,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 8,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -374,7 +356,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 9,
|
ProjectID: 9,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 9,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -387,7 +368,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 10,
|
ProjectID: 10,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 10,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -400,7 +380,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 11,
|
ProjectID: 11,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 11,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -413,7 +392,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 32, // parent project is shared to user 1 via direct share
|
ProjectID: 32, // parent project is shared to user 1 via direct share
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 12,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -426,7 +404,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 33,
|
ProjectID: 33,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 36,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -439,7 +416,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 34,
|
ProjectID: 34,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 37,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -452,7 +428,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 15, // parent project is shared to user 1 via team
|
ProjectID: 15, // parent project is shared to user 1 via team
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 15,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -465,7 +440,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 16,
|
ProjectID: 16,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 16,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -478,7 +452,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user6,
|
CreatedBy: user6,
|
||||||
ProjectID: 17,
|
ProjectID: 17,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 17,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -507,7 +480,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
},
|
},
|
||||||
StartDate: time.Unix(1543616724, 0).In(loc),
|
StartDate: time.Unix(1543616724, 0).In(loc),
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
BucketID: 1,
|
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
@ -522,7 +494,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
RepeatAfter: 3600,
|
RepeatAfter: 3600,
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -546,14 +517,11 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
IsFavorite: true,
|
IsFavorite: true,
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
BucketID: 1,
|
|
||||||
Position: 2,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BucketID: 1,
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
|
||||||
}
|
}
|
||||||
task30 := &Task{
|
task30 := &Task{
|
||||||
ID: 30,
|
ID: 30,
|
||||||
@ -568,7 +536,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
user2,
|
user2,
|
||||||
},
|
},
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -582,7 +549,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -595,7 +561,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 3,
|
ProjectID: 3,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 21,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -609,7 +574,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
ProjectID: 1,
|
ProjectID: 1,
|
||||||
PercentDone: 0.5,
|
PercentDone: 0.5,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 1,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
@ -639,8 +603,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
IsFavorite: true,
|
IsFavorite: true,
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
BucketID: 1,
|
|
||||||
Position: 2,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
@ -652,14 +614,11 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
IsFavorite: true,
|
IsFavorite: true,
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
BucketID: 1,
|
|
||||||
Position: 2,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BucketID: 19,
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
|
||||||
}
|
}
|
||||||
task39 := &Task{
|
task39 := &Task{
|
||||||
ID: 39,
|
ID: 39,
|
||||||
@ -669,16 +628,16 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
CreatedBy: user1,
|
CreatedBy: user1,
|
||||||
ProjectID: 25,
|
ProjectID: 25,
|
||||||
RelatedTasks: map[RelationKind][]*Task{},
|
RelatedTasks: map[RelationKind][]*Task{},
|
||||||
BucketID: 0,
|
|
||||||
Created: time.Unix(1543626724, 0).In(loc),
|
Created: time.Unix(1543626724, 0).In(loc),
|
||||||
Updated: time.Unix(1543626724, 0).In(loc),
|
Updated: time.Unix(1543626724, 0).In(loc),
|
||||||
}
|
}
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
ProjectID int64
|
ProjectID int64
|
||||||
Projects []*Project
|
ProjectViewID int64
|
||||||
SortBy []string // Is a string, since this is the place where a query string comes from the user
|
Projects []*Project
|
||||||
OrderBy []string
|
SortBy []string // Is a string, since this is the place where a query string comes from the user
|
||||||
|
OrderBy []string
|
||||||
|
|
||||||
FilterIncludeNulls bool
|
FilterIncludeNulls bool
|
||||||
Filter string
|
Filter string
|
||||||
@ -705,6 +664,13 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
page: 0,
|
page: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
taskWithPosition := func(task *Task, position float64) *Task {
|
||||||
|
newTask := &Task{}
|
||||||
|
*newTask = *task
|
||||||
|
newTask.Position = position
|
||||||
|
return newTask
|
||||||
|
}
|
||||||
|
|
||||||
tests := []testcase{
|
tests := []testcase{
|
||||||
{
|
{
|
||||||
name: "ReadAll Tasks normally",
|
name: "ReadAll Tasks normally",
|
||||||
@ -1258,16 +1224,18 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "order by position",
|
name: "order by position",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
SortBy: []string{"position", "id"},
|
SortBy: []string{"position", "id"},
|
||||||
OrderBy: []string{"asc", "asc"},
|
OrderBy: []string{"asc", "asc"},
|
||||||
|
ProjectViewID: 1,
|
||||||
|
ProjectID: 1,
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
a: &user.User{ID: 1},
|
a: &user.User{ID: 1},
|
||||||
},
|
},
|
||||||
want: []*Task{
|
want: []*Task{
|
||||||
// The only tasks with a position set
|
// The only tasks with a position set
|
||||||
task1,
|
taskWithPosition(task1, 2),
|
||||||
task2,
|
taskWithPosition(task2, 4),
|
||||||
// the other ones don't have a position set
|
// the other ones don't have a position set
|
||||||
task3,
|
task3,
|
||||||
task4,
|
task4,
|
||||||
@ -1279,27 +1247,24 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
task10,
|
task10,
|
||||||
task11,
|
task11,
|
||||||
task12,
|
task12,
|
||||||
task15,
|
//task15,
|
||||||
task16,
|
//task16,
|
||||||
task17,
|
//task17,
|
||||||
task18,
|
//task18,
|
||||||
task19,
|
//task19,
|
||||||
task20,
|
//task20,
|
||||||
task21,
|
//task21,
|
||||||
task22,
|
//task22,
|
||||||
task23,
|
//task23,
|
||||||
task24,
|
//task24,
|
||||||
task25,
|
//task25,
|
||||||
task26,
|
//task26,
|
||||||
task27,
|
task27,
|
||||||
task28,
|
task28,
|
||||||
task29,
|
task29,
|
||||||
task30,
|
task30,
|
||||||
task31,
|
task31,
|
||||||
task32,
|
|
||||||
task33,
|
task33,
|
||||||
task35,
|
|
||||||
task39,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1414,9 +1379,10 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
lt := &TaskCollection{
|
lt := &TaskCollection{
|
||||||
ProjectID: tt.fields.ProjectID,
|
ProjectID: tt.fields.ProjectID,
|
||||||
SortBy: tt.fields.SortBy,
|
ProjectViewID: tt.fields.ProjectViewID,
|
||||||
OrderBy: tt.fields.OrderBy,
|
SortBy: tt.fields.SortBy,
|
||||||
|
OrderBy: tt.fields.OrderBy,
|
||||||
|
|
||||||
FilterIncludeNulls: tt.fields.FilterIncludeNulls,
|
FilterIncludeNulls: tt.fields.FilterIncludeNulls,
|
||||||
|
|
||||||
|
180
pkg/models/task_position.go
Normal file
180
pkg/models/task_position.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
// Vikunja is a to-do list application to facilitate your life.
|
||||||
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public Licensee for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public Licensee
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskPosition struct {
|
||||||
|
// The ID of the task this position is for
|
||||||
|
TaskID int64 `xorm:"bigint not null index" json:"task_id" param:"task"`
|
||||||
|
// The project view this task is related to
|
||||||
|
ProjectViewID int64 `xorm:"bigint not null index" json:"project_view_id"`
|
||||||
|
// The position of the task - any task project can be sorted as usual by this parameter.
|
||||||
|
// When accessing tasks via kanban buckets, this is primarily used to sort them based on a range
|
||||||
|
// We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number).
|
||||||
|
// You would calculate the new position between two tasks with something like task3.position = (task2.position - task1.position) / 2.
|
||||||
|
// A 64-Bit float leaves plenty of room to initially give tasks a position with 2^16 difference to the previous task
|
||||||
|
// which also leaves a lot of room for rearranging and sorting later.
|
||||||
|
// Positions are always saved per view. They will automatically be set if you request the tasks through a view
|
||||||
|
// endpoint, otherwise they will always be 0. To update them, take a look at the Task Position endpoint.
|
||||||
|
Position float64 `xorm:"double not null" json:"position"`
|
||||||
|
|
||||||
|
web.CRUDable `xorm:"-" json:"-"`
|
||||||
|
web.Rights `xorm:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp *TaskPosition) TableName() string {
|
||||||
|
return "task_positions"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp *TaskPosition) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||||
|
pv, err := GetProjectViewByID(s, tp.ProjectViewID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return pv.CanUpdate(s, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update is the handler to update a task position
|
||||||
|
// @Summary Updates a task position
|
||||||
|
// @Description Updates a task position.
|
||||||
|
// @tags task
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWTKeyAuth
|
||||||
|
// @Param id path int true "Task ID"
|
||||||
|
// @Param view body models.TaskPosition true "The task position with updated values you want to change."
|
||||||
|
// @Success 200 {object} models.TaskPosition "The updated task position."
|
||||||
|
// @Failure 400 {object} web.HTTPError "Invalid task position object provided."
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /tasks/{id}/position [post]
|
||||||
|
func (tp *TaskPosition) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||||
|
|
||||||
|
// Update all positions if the newly saved position is < 0.1
|
||||||
|
var shouldRecalculate bool
|
||||||
|
var view *ProjectView
|
||||||
|
if tp.Position < 0.1 {
|
||||||
|
shouldRecalculate = true
|
||||||
|
view, err = GetProjectViewByID(s, tp.ProjectViewID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := s.
|
||||||
|
Where("task_id = ? AND project_view_id = ?", tp.TaskID, tp.ProjectViewID).
|
||||||
|
Exist(&TaskPosition{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
_, err = s.Insert(tp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if shouldRecalculate {
|
||||||
|
return RecalculateTaskPositions(s, view, a)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.
|
||||||
|
Where("task_id = ?", tp.TaskID).
|
||||||
|
Cols("project_view_id", "position").
|
||||||
|
Update(tp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldRecalculate {
|
||||||
|
return RecalculateTaskPositions(s, view, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecalculateTaskPositions(s *xorm.Session, view *ProjectView, a web.Auth) (err error) {
|
||||||
|
|
||||||
|
// Using the collection so that we get all tasks, even in cases where we're dealing with a saved filter underneath
|
||||||
|
tc := &TaskCollection{
|
||||||
|
ProjectID: view.ProjectID,
|
||||||
|
}
|
||||||
|
if view.ProjectID < -1 {
|
||||||
|
tc.ProjectID = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
projects, err := getRelevantProjectsFromCollection(s, a, tc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &taskSearchOptions{
|
||||||
|
sortby: []*sortParam{
|
||||||
|
{
|
||||||
|
projectViewID: view.ID,
|
||||||
|
sortBy: taskPropertyPosition,
|
||||||
|
orderBy: orderAscending,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
allTasks, _, _, err := getRawTasksForProjects(s, projects, a, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(allTasks) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxPosition := math.Pow(2, 32)
|
||||||
|
newPositions := make([]*TaskPosition, 0, len(allTasks))
|
||||||
|
|
||||||
|
for i, task := range allTasks {
|
||||||
|
|
||||||
|
currentPosition := maxPosition / float64(len(allTasks)) * (float64(i + 1))
|
||||||
|
|
||||||
|
newPositions = append(newPositions, &TaskPosition{
|
||||||
|
TaskID: task.ID,
|
||||||
|
ProjectViewID: view.ID,
|
||||||
|
Position: currentPosition,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.
|
||||||
|
Where("project_view_id = ?", view.ID).
|
||||||
|
Delete(&TaskPosition{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.Insert(newPositions)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPositionsForView(s *xorm.Session, view *ProjectView) (positions []*TaskPosition, err error) {
|
||||||
|
positions = []*TaskPosition{}
|
||||||
|
err = s.
|
||||||
|
Where("project_view_id = ?", view.ID).
|
||||||
|
Find(&positions)
|
||||||
|
return
|
||||||
|
}
|
@ -53,14 +53,19 @@ func getOrderByDBStatement(opts *taskSearchOptions) (orderby string, err error)
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prefix string
|
||||||
|
if param.sortBy == taskPropertyPosition {
|
||||||
|
prefix = "task_positions."
|
||||||
|
}
|
||||||
|
|
||||||
// Mysql sorts columns with null values before ones without null value.
|
// Mysql sorts columns with null values before ones without null value.
|
||||||
// Because it does not have support for NULLS FIRST or NULLS LAST we work around this by
|
// Because it does not have support for NULLS FIRST or NULLS LAST we work around this by
|
||||||
// first sorting for null (or not null) values and then the order we actually want to.
|
// first sorting for null (or not null) values and then the order we actually want to.
|
||||||
if db.Type() == schemas.MYSQL {
|
if db.Type() == schemas.MYSQL {
|
||||||
orderby += "`" + param.sortBy + "` IS NULL, "
|
orderby += prefix + "`" + param.sortBy + "` IS NULL, "
|
||||||
}
|
}
|
||||||
|
|
||||||
orderby += "`" + param.sortBy + "` " + param.orderBy.String()
|
orderby += prefix + "`" + param.sortBy + "` " + param.orderBy.String()
|
||||||
|
|
||||||
// Postgres and sqlite allow us to control how columns with null values are sorted.
|
// Postgres and sqlite allow us to control how columns with null values are sorted.
|
||||||
// To make that consistent with the sort order we have and other dbms, we're adding a separate clause here.
|
// To make that consistent with the sort order we have and other dbms, we're adding a separate clause here.
|
||||||
@ -204,6 +209,14 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var joinTaskBuckets bool
|
||||||
|
for _, filter := range opts.parsedFilters {
|
||||||
|
if filter.field == taskPropertyBucketID {
|
||||||
|
joinTaskBuckets = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
filterCond, err := convertFiltersToDBFilterCond(opts.parsedFilters, opts.filterIncludeNulls)
|
filterCond, err := convertFiltersToDBFilterCond(opts.parsedFilters, opts.filterIncludeNulls)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@ -248,25 +261,43 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||||
cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond)
|
cond := builder.And(builder.Or(projectIDCond, favoritesCond), where, filterCond)
|
||||||
|
|
||||||
query := d.s.Where(cond)
|
var distinct = "tasks.*"
|
||||||
|
if strings.Contains(orderby, "task_positions.") {
|
||||||
|
distinct += ", task_positions.position"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := d.s.
|
||||||
|
Distinct(distinct).
|
||||||
|
Where(cond)
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
query = query.Limit(limit, start)
|
query = query.Limit(limit, start)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, param := range opts.sortby {
|
||||||
|
if param.sortBy == taskPropertyPosition {
|
||||||
|
query = query.Join("LEFT", "task_positions", "task_positions.task_id = tasks.id AND task_positions.project_view_id = ?", param.projectViewID)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if joinTaskBuckets {
|
||||||
|
query = query.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id")
|
||||||
|
}
|
||||||
|
|
||||||
tasks = []*Task{}
|
tasks = []*Task{}
|
||||||
err = query.OrderBy(orderby).Find(&tasks)
|
err = query.
|
||||||
|
OrderBy(orderby).
|
||||||
|
Find(&tasks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, totalCount, err
|
return nil, totalCount, err
|
||||||
}
|
}
|
||||||
|
|
||||||
queryCount := d.s.Where(cond)
|
queryCount := d.s.Where(cond)
|
||||||
|
if joinTaskBuckets {
|
||||||
|
queryCount = queryCount.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id")
|
||||||
|
}
|
||||||
totalCount, err = queryCount.
|
totalCount, err = queryCount.
|
||||||
Count(&Task{})
|
Count(&Task{})
|
||||||
if err != nil {
|
|
||||||
return nil, totalCount, err
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,29 +435,6 @@ func convertParsedFilterToTypesense(rawFilters []*taskFilter) (filterBy string,
|
|||||||
|
|
||||||
func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCount int64, err error) {
|
||||||
|
|
||||||
var sortbyFields []string
|
|
||||||
for i, param := range opts.sortby {
|
|
||||||
// Validate the params
|
|
||||||
if err := param.validate(); err != nil {
|
|
||||||
return nil, totalCount, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
|
|
||||||
if param.sortBy == "id" {
|
|
||||||
param.sortBy = "created"
|
|
||||||
}
|
|
||||||
|
|
||||||
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
|
|
||||||
|
|
||||||
if i == 2 {
|
|
||||||
// Typesense supports up to 3 sorting parameters
|
|
||||||
// https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sortby := strings.Join(sortbyFields, ",")
|
|
||||||
|
|
||||||
projectIDStrings := []string{}
|
projectIDStrings := []string{}
|
||||||
for _, id := range opts.projectIDs {
|
for _, id := range opts.projectIDs {
|
||||||
projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10))
|
projectIDStrings = append(projectIDStrings, strconv.FormatInt(id, 10))
|
||||||
@ -442,6 +450,34 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
|||||||
"(" + filter + ")",
|
"(" + filter + ")",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sortbyFields []string
|
||||||
|
for i, param := range opts.sortby {
|
||||||
|
// Validate the params
|
||||||
|
if err := param.validate(); err != nil {
|
||||||
|
return nil, totalCount, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
|
||||||
|
if param.sortBy == taskPropertyID {
|
||||||
|
param.sortBy = taskPropertyCreated
|
||||||
|
}
|
||||||
|
|
||||||
|
if param.sortBy == taskPropertyPosition {
|
||||||
|
param.sortBy = "positions.view_" + strconv.FormatInt(param.projectViewID, 10)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
|
||||||
|
|
||||||
|
if i == 2 {
|
||||||
|
// Typesense supports up to 3 sorting parameters
|
||||||
|
// https://typesense.org/docs/0.25.0/api/search.html#ranking-and-sorting-parameters
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortby := strings.Join(sortbyFields, ",")
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
// Actual search
|
// Actual search
|
||||||
|
|
||||||
|
@ -112,18 +112,15 @@ type Task struct {
|
|||||||
// A timestamp when this task was last updated. You cannot change this value.
|
// A timestamp when this task was last updated. You cannot change this value.
|
||||||
Updated time.Time `xorm:"updated not null" json:"updated"`
|
Updated time.Time `xorm:"updated not null" json:"updated"`
|
||||||
|
|
||||||
// BucketID is the ID of the kanban bucket this task belongs to.
|
// The bucket id. Will only be populated when the task is accessed via a view with buckets.
|
||||||
BucketID int64 `xorm:"bigint null" json:"bucket_id"`
|
// Can be used to move a task between buckets. In that case, the new bucket must be in the same view as the old one.
|
||||||
|
BucketID int64 `xorm:"<-" json:"bucket_id"`
|
||||||
|
|
||||||
// The position of the task - any task project can be sorted as usual by this parameter.
|
// The position of the task - any task project can be sorted as usual by this parameter.
|
||||||
// When accessing tasks via kanban buckets, this is primarily used to sort them based on a range
|
// When accessing tasks via views with buckets, this is primarily used to sort them based on a range.
|
||||||
// We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number).
|
// Positions are always saved per view. They will automatically be set if you request the tasks through a view
|
||||||
// You would calculate the new position between two tasks with something like task3.position = (task2.position - task1.position) / 2.
|
// endpoint, otherwise they will always be 0. To update them, take a look at the Task Position endpoint.
|
||||||
// A 64-Bit float leaves plenty of room to initially give tasks a position with 2^16 difference to the previous task
|
Position float64 `xorm:"-" json:"position"`
|
||||||
// which also leaves a lot of room for rearranging and sorting later.
|
|
||||||
Position float64 `xorm:"double null" json:"position"`
|
|
||||||
// The position of tasks in the kanban board. See the docs for the `position` property on how to use this.
|
|
||||||
KanbanPosition float64 `xorm:"double null" json:"kanban_position"`
|
|
||||||
|
|
||||||
// Reactions on that task.
|
// Reactions on that task.
|
||||||
Reactions ReactionMap `xorm:"-" json:"reactions"`
|
Reactions ReactionMap `xorm:"-" json:"reactions"`
|
||||||
@ -207,6 +204,9 @@ func (t *Task) ReadAll(_ *xorm.Session, _ web.Auth, _ string, _ int, _ int) (res
|
|||||||
|
|
||||||
func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err error) {
|
func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err error) {
|
||||||
field := "`" + f.field + "`"
|
field := "`" + f.field + "`"
|
||||||
|
if f.field == taskPropertyBucketID {
|
||||||
|
field = "task_buckets.`bucket_id`"
|
||||||
|
}
|
||||||
switch f.comparator {
|
switch f.comparator {
|
||||||
case taskFilterComparatorEquals:
|
case taskFilterComparatorEquals:
|
||||||
cond = &builder.Eq{field: f.value}
|
cond = &builder.Eq{field: f.value}
|
||||||
@ -261,7 +261,6 @@ func getTaskIndexFromSearchString(s string) (index int64) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
|
||||||
func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||||
|
|
||||||
// If the user does not have any projects, don't try to get any tasks
|
// If the user does not have any projects, don't try to get any tasks
|
||||||
@ -304,7 +303,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
|
|||||||
return tasks, len(tasks), totalItems, err
|
return tasks, len(tasks), totalItems, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts *taskSearchOptions, view *ProjectView) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||||
|
|
||||||
tasks, resultCount, totalItems, err = getRawTasksForProjects(s, projects, a, opts)
|
tasks, resultCount, totalItems, err = getRawTasksForProjects(s, projects, a, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -316,7 +315,7 @@ func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts
|
|||||||
taskMap[t.ID] = t
|
taskMap[t.ID] = t
|
||||||
}
|
}
|
||||||
|
|
||||||
err = addMoreInfoToTasks(s, taskMap, a)
|
err = addMoreInfoToTasks(s, taskMap, a, view)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
@ -393,7 +392,7 @@ func GetTasksByUIDs(s *xorm.Session, uids []string, a web.Auth) (tasks []*Task,
|
|||||||
taskMap[t.ID] = t
|
taskMap[t.ID] = t
|
||||||
}
|
}
|
||||||
|
|
||||||
err = addMoreInfoToTasks(s, taskMap, a)
|
err = addMoreInfoToTasks(s, taskMap, a, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,7 +533,7 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
|
|||||||
|
|
||||||
// This function takes a map with pointers and returns a slice with pointers to tasks
|
// This function takes a map with pointers and returns a slice with pointers to tasks
|
||||||
// It adds more stuff like assignees/labels/etc to a bunch of tasks
|
// It adds more stuff like assignees/labels/etc to a bunch of tasks
|
||||||
func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (err error) {
|
func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, view *ProjectView) (err error) {
|
||||||
|
|
||||||
// No need to iterate over users and stuff if the project doesn't have tasks
|
// No need to iterate over users and stuff if the project doesn't have tasks
|
||||||
if len(taskMap) == 0 {
|
if len(taskMap) == 0 {
|
||||||
@ -592,6 +591,17 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var positionsMap = make(map[int64]*TaskPosition)
|
||||||
|
if view != nil {
|
||||||
|
positions, err := getPositionsForView(s, view)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, position := range positions {
|
||||||
|
positionsMap[position.TaskID] = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add all objects to their tasks
|
// Add all objects to their tasks
|
||||||
for _, task := range taskMap {
|
for _, task := range taskMap {
|
||||||
|
|
||||||
@ -613,6 +623,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||||||
if has {
|
if has {
|
||||||
task.Reactions = r
|
task.Reactions = r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p, has := positionsMap[task.ID]
|
||||||
|
if has {
|
||||||
|
task.Position = p.Position
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all related tasks
|
// Get all related tasks
|
||||||
@ -620,23 +635,12 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBucketAndTaskBelongToSameProject(fullTask *Task, bucket *Bucket) (err error) {
|
|
||||||
if fullTask.ProjectID != bucket.ProjectID {
|
|
||||||
return ErrBucketDoesNotBelongToProject{
|
|
||||||
ProjectID: fullTask.ProjectID,
|
|
||||||
BucketID: fullTask.BucketID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if adding a new task would exceed the bucket limit
|
// Checks if adding a new task would exceed the bucket limit
|
||||||
func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
|
func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
|
||||||
if bucket.Limit > 0 {
|
if bucket.Limit > 0 {
|
||||||
taskCount, err := s.
|
taskCount, err := s.
|
||||||
Where("bucket_id = ?", bucket.ID).
|
Where("bucket_id = ?", bucket.ID).
|
||||||
Count(&Task{})
|
Count(&TaskBucket{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -648,62 +652,92 @@ func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Contains all the task logic to figure out what bucket to use for this task.
|
// Contains all the task logic to figure out what bucket to use for this task.
|
||||||
func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucketLimit bool, project *Project) (targetBucket *Bucket, err error) {
|
func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, view *ProjectView, targetBucketID int64) (err error) {
|
||||||
|
if view.BucketConfigurationMode == BucketConfigurationModeNone {
|
||||||
if project == nil {
|
return
|
||||||
project, err = GetProjectSimpleByID(s, task.ProjectID)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
var shouldChangeBucket = true
|
||||||
}
|
targetBucket := &TaskBucket{
|
||||||
|
BucketID: targetBucketID,
|
||||||
|
TaskID: task.ID,
|
||||||
|
ProjectViewID: view.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTaskBucket := &TaskBucket{}
|
||||||
|
_, err = s.
|
||||||
|
Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).
|
||||||
|
Get(oldTaskBucket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var bucket *Bucket
|
|
||||||
if task.Done && originalTask != nil &&
|
if task.Done && originalTask != nil &&
|
||||||
(!originalTask.Done || task.ProjectID != originalTask.ProjectID) {
|
(!originalTask.Done || task.ProjectID != originalTask.ProjectID) {
|
||||||
task.BucketID = project.DoneBucketID
|
targetBucket.BucketID = view.DoneBucketID
|
||||||
}
|
}
|
||||||
|
|
||||||
if task.BucketID == 0 && originalTask != nil && originalTask.BucketID != 0 {
|
if targetBucket.BucketID == 0 && oldTaskBucket.BucketID != 0 {
|
||||||
task.BucketID = originalTask.BucketID
|
shouldChangeBucket = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either no bucket was provided or the task was moved between projects
|
// Either no bucket was provided or the task was moved between projects
|
||||||
// But if the task was moved between projects, don't update the done bucket
|
// But if the task was moved between projects, don't update the done bucket
|
||||||
// because then we have it already updated to the done bucket.
|
// because then we have it already updated to the done bucket.
|
||||||
if task.BucketID == 0 ||
|
if targetBucket.BucketID == 0 ||
|
||||||
(originalTask != nil && task.ProjectID != 0 && originalTask.ProjectID != task.ProjectID && !task.Done) {
|
(originalTask != nil && task.ProjectID != 0 && originalTask.ProjectID != task.ProjectID && !task.Done) {
|
||||||
task.BucketID, err = getDefaultBucketID(s, project)
|
targetBucket.BucketID, err = getDefaultBucketID(s, view)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bucket == nil {
|
bucket, err := getBucketByID(s, targetBucket.BucketID)
|
||||||
bucket, err = getBucketByID(s, task.BucketID)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a bucket set, make sure they belong to the same project as the task
|
// If there is a bucket set, make sure they belong to the same project as the task
|
||||||
err = checkBucketAndTaskBelongToSameProject(task, bucket)
|
if view.ID != bucket.ProjectViewID {
|
||||||
if err != nil {
|
return ErrBucketDoesNotBelongToProjectView{
|
||||||
return
|
ProjectViewID: view.ID,
|
||||||
|
BucketID: bucket.ID,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the bucket limit
|
// Check the bucket limit
|
||||||
// Only check the bucket limit if the task is being moved between buckets, allow reordering the task within a bucket
|
// Only check the bucket limit if the task is being moved between buckets, allow reordering the task within a bucket
|
||||||
if doCheckBucketLimit {
|
if targetBucket.BucketID != 0 && targetBucket.BucketID != oldTaskBucket.BucketID {
|
||||||
if err := checkBucketLimit(s, task, bucket); err != nil {
|
err = checkBucketLimit(s, task, bucket)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bucket.ID == project.DoneBucketID && originalTask != nil && !originalTask.Done {
|
if bucket.ID == view.DoneBucketID && originalTask != nil && !originalTask.Done {
|
||||||
task.Done = true
|
task.Done = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return bucket, nil
|
// If the task was moved into the done bucket and the task has a repeating cycle we should not update
|
||||||
|
// the bucket.
|
||||||
|
if bucket.ID == view.DoneBucketID && task.RepeatAfter > 0 {
|
||||||
|
task.Done = true // This will trigger the correct re-scheduling of the task (happening in updateDone later)
|
||||||
|
shouldChangeBucket = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldChangeBucket {
|
||||||
|
_, err = s.
|
||||||
|
Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).
|
||||||
|
Delete(&TaskBucket{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetBucket.BucketID = bucket.ID
|
||||||
|
_, err = s.Insert(targetBucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateDefaultPosition(entityID int64, position float64) float64 {
|
func calculateDefaultPosition(entityID int64, position float64) float64 {
|
||||||
@ -742,10 +776,10 @@ func getNextTaskIndex(s *xorm.Session, projectID int64) (nextIndex int64, err er
|
|||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /projects/{id}/tasks [put]
|
// @Router /projects/{id}/tasks [put]
|
||||||
func (t *Task) Create(s *xorm.Session, a web.Auth) (err error) {
|
func (t *Task) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||||
return createTask(s, t, a, true)
|
return createTask(s, t, a, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err error) {
|
func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool, updateBucket bool) (err error) {
|
||||||
|
|
||||||
t.ID = 0
|
t.ID = 0
|
||||||
|
|
||||||
@ -771,22 +805,12 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||||||
t.UID = uuid.NewString()
|
t.UID = uuid.NewString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the default bucket and move the task there
|
|
||||||
_, err = setTaskBucket(s, t, nil, true, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the index for this task
|
// Get the index for this task
|
||||||
t.Index, err = getNextTaskIndex(s, t.ProjectID)
|
t.Index, err = getNextTaskIndex(s, t.ProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no position was supplied, set a default one
|
|
||||||
t.Position = calculateDefaultPosition(t.Index, t.Position)
|
|
||||||
t.KanbanPosition = calculateDefaultPosition(t.Index, t.KanbanPosition)
|
|
||||||
|
|
||||||
t.HexColor = utils.NormalizeHex(t.HexColor)
|
t.HexColor = utils.NormalizeHex(t.HexColor)
|
||||||
|
|
||||||
_, err = s.Insert(t)
|
_, err = s.Insert(t)
|
||||||
@ -794,6 +818,37 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views, err := getViewsForProject(s, t.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
positions := []*TaskPosition{}
|
||||||
|
|
||||||
|
for _, view := range views {
|
||||||
|
|
||||||
|
if updateBucket {
|
||||||
|
// Get the default bucket and move the task there
|
||||||
|
err = setTaskBucket(s, t, nil, view, t.BucketID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
positions = append(positions, &TaskPosition{
|
||||||
|
TaskID: t.ID,
|
||||||
|
ProjectViewID: view.ID,
|
||||||
|
Position: calculateDefaultPosition(t.Index, t.Position),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateBucket {
|
||||||
|
_, err = s.Insert(&positions)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
t.CreatedBy = createdBy
|
t.CreatedBy = createdBy
|
||||||
|
|
||||||
// Update the assignees
|
// Update the assignees
|
||||||
@ -865,26 +920,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
// Old task has the stored reminders
|
// Old task has the stored reminders
|
||||||
ot.Reminders = reminders
|
ot.Reminders = reminders
|
||||||
|
|
||||||
project, err := GetProjectSimpleByID(s, t.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
targetBucket, err := setTaskBucket(s, t, &ot, t.BucketID != 0 && t.BucketID != ot.BucketID, project)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the task was moved into the done bucket and the task has a repeating cycle we should not update
|
|
||||||
// the bucket.
|
|
||||||
if targetBucket.ID == project.DoneBucketID && t.RepeatAfter > 0 {
|
|
||||||
t.Done = true // This will trigger the correct re-scheduling of the task (happening in updateDone later)
|
|
||||||
t.BucketID = ot.BucketID
|
|
||||||
}
|
|
||||||
|
|
||||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
|
||||||
updateDone(&ot, t)
|
|
||||||
|
|
||||||
// Update the assignees
|
// Update the assignees
|
||||||
if err := ot.updateTaskAssignees(s, t.Assignees, a); err != nil {
|
if err := ot.updateTaskAssignees(s, t.Assignees, a); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -910,9 +945,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
"percent_done",
|
"percent_done",
|
||||||
"project_id",
|
"project_id",
|
||||||
"bucket_id",
|
"bucket_id",
|
||||||
"position",
|
|
||||||
"repeat_mode",
|
"repeat_mode",
|
||||||
"kanban_position",
|
|
||||||
"cover_image_attachment_id",
|
"cover_image_attachment_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -922,9 +955,48 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
t.BucketID = 0
|
||||||
colsToUpdate = append(colsToUpdate, "index")
|
colsToUpdate = append(colsToUpdate, "index")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views, err := getViewsForProject(s, t.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buckets := make(map[int64]*Bucket)
|
||||||
|
err = s.In("project_view_id",
|
||||||
|
builder.Select("id").
|
||||||
|
From("project_views").
|
||||||
|
Where(builder.Eq{"project_id": t.ProjectID}),
|
||||||
|
).
|
||||||
|
Find(&buckets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, view := range views {
|
||||||
|
// Only update the bucket when the current view
|
||||||
|
var targetBucketID int64
|
||||||
|
if t.BucketID != 0 {
|
||||||
|
bucket, has := buckets[t.BucketID]
|
||||||
|
if !has {
|
||||||
|
return ErrBucketDoesNotExist{BucketID: t.BucketID}
|
||||||
|
}
|
||||||
|
if has && bucket.ProjectViewID == view.ID {
|
||||||
|
targetBucketID = t.BucketID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setTaskBucket(s, t, &ot, view, targetBucketID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||||
|
updateDone(&ot, t)
|
||||||
|
|
||||||
// If a task attachment is being set as cover image, check if the attachment actually belongs to the task
|
// If a task attachment is being set as cover image, check if the attachment actually belongs to the task
|
||||||
if t.CoverImageAttachmentID != 0 {
|
if t.CoverImageAttachmentID != 0 {
|
||||||
is, err := s.Exist(&TaskAttachment{
|
is, err := s.Exist(&TaskAttachment{
|
||||||
@ -1024,13 +1096,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
if t.PercentDone == 0 {
|
if t.PercentDone == 0 {
|
||||||
ot.PercentDone = 0
|
ot.PercentDone = 0
|
||||||
}
|
}
|
||||||
// Position
|
|
||||||
if t.Position == 0 {
|
|
||||||
ot.Position = 0
|
|
||||||
}
|
|
||||||
if t.KanbanPosition == 0 {
|
|
||||||
ot.KanbanPosition = 0
|
|
||||||
}
|
|
||||||
// Repeat from current date
|
// Repeat from current date
|
||||||
if t.RepeatMode == TaskRepeatModeDefault {
|
if t.RepeatMode == TaskRepeatModeDefault {
|
||||||
ot.RepeatMode = TaskRepeatModeDefault
|
ot.RepeatMode = TaskRepeatModeDefault
|
||||||
@ -1052,20 +1117,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update all positions if the newly saved position is < 0.1
|
|
||||||
if ot.Position < 0.1 {
|
|
||||||
err = recalculateTaskPositions(s, t.ProjectID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ot.KanbanPosition < 0.1 {
|
|
||||||
err = recalculateTaskKanbanPositions(s, t.BucketID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the task updated timestamp in a new struct - if we'd just try to put it into t which we already have, it
|
// Get the task updated timestamp in a new struct - if we'd just try to put it into t which we already have, it
|
||||||
// would still contain the old updated date.
|
// would still contain the old updated date.
|
||||||
nt := &Task{}
|
nt := &Task{}
|
||||||
@ -1074,8 +1125,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.Updated = nt.Updated
|
t.Updated = nt.Updated
|
||||||
t.Position = nt.Position
|
|
||||||
t.KanbanPosition = nt.KanbanPosition
|
|
||||||
|
|
||||||
doer, _ := user.GetFromAuth(a)
|
doer, _ := user.GetFromAuth(a)
|
||||||
err = events.Dispatch(&TaskUpdatedEvent{
|
err = events.Dispatch(&TaskUpdatedEvent{
|
||||||
@ -1089,72 +1138,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
return updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
return updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||||
}
|
}
|
||||||
|
|
||||||
func recalculateTaskKanbanPositions(s *xorm.Session, bucketID int64) (err error) {
|
|
||||||
|
|
||||||
allTasks := []*Task{}
|
|
||||||
err = s.
|
|
||||||
Where("bucket_id = ?", bucketID).
|
|
||||||
OrderBy("kanban_position asc").
|
|
||||||
Find(&allTasks)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
maxPosition := math.Pow(2, 32)
|
|
||||||
|
|
||||||
for i, task := range allTasks {
|
|
||||||
|
|
||||||
currentPosition := maxPosition / float64(len(allTasks)) * (float64(i + 1))
|
|
||||||
|
|
||||||
// Here we use "NoAutoTime() to prevent the ORM from updating column "updated" automatically.
|
|
||||||
// Otherwise, this signals to CalDAV clients that the task has changed, which is not the case.
|
|
||||||
// Consequence: when synchronizing a list of tasks, the first one immediately changes the date of all the
|
|
||||||
// following ones from the same batch, which are then unable to be updated.
|
|
||||||
_, err = s.Cols("kanban_position").
|
|
||||||
Where("id = ?", task.ID).
|
|
||||||
NoAutoTime().
|
|
||||||
Update(&Task{KanbanPosition: currentPosition})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func recalculateTaskPositions(s *xorm.Session, projectID int64) (err error) {
|
|
||||||
|
|
||||||
allTasks := []*Task{}
|
|
||||||
err = s.
|
|
||||||
Where("project_id = ?", projectID).
|
|
||||||
OrderBy("position asc").
|
|
||||||
Find(&allTasks)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
maxPosition := math.Pow(2, 32)
|
|
||||||
|
|
||||||
for i, task := range allTasks {
|
|
||||||
|
|
||||||
currentPosition := maxPosition / float64(len(allTasks)) * (float64(i + 1))
|
|
||||||
|
|
||||||
// Here we use "NoAutoTime() to prevent the ORM from updating column "updated" automatically.
|
|
||||||
// Otherwise, this signals to CalDAV clients that the task has changed, which is not the case.
|
|
||||||
// Consequence: when synchronizing a list of tasks, the first one immediately changes the date of all the
|
|
||||||
// following ones from the same batch, which are then unable to be updated.
|
|
||||||
_, err = s.Cols("position").
|
|
||||||
Where("id = ?", task.ID).
|
|
||||||
NoAutoTime().
|
|
||||||
Update(&Task{Position: currentPosition})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func addOneMonthToDate(d time.Time) time.Time {
|
func addOneMonthToDate(d time.Time) time.Time {
|
||||||
return time.Date(d.Year(), d.Month()+1, d.Day(), d.Hour(), d.Minute(), d.Second(), d.Nanosecond(), config.GetTimeZone())
|
return time.Date(d.Year(), d.Month()+1, d.Day(), d.Hour(), d.Minute(), d.Second(), d.Nanosecond(), config.GetTimeZone())
|
||||||
}
|
}
|
||||||
@ -1531,7 +1514,7 @@ func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||||||
taskMap := make(map[int64]*Task, 1)
|
taskMap := make(map[int64]*Task, 1)
|
||||||
taskMap[t.ID] = t
|
taskMap[t.ID] = t
|
||||||
|
|
||||||
err = addMoreInfoToTasks(s, taskMap, a)
|
err = addMoreInfoToTasks(s, taskMap, a, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -55,8 +55,6 @@ func TestTask_Create(t *testing.T) {
|
|||||||
// Assert getting a new index
|
// Assert getting a new index
|
||||||
assert.NotEmpty(t, task.Index)
|
assert.NotEmpty(t, task.Index)
|
||||||
assert.Equal(t, int64(18), task.Index)
|
assert.Equal(t, int64(18), task.Index)
|
||||||
// Assert moving it into the default bucket
|
|
||||||
assert.Equal(t, int64(1), task.BucketID)
|
|
||||||
err = s.Commit()
|
err = s.Commit()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -66,7 +64,10 @@ func TestTask_Create(t *testing.T) {
|
|||||||
"description": "Lorem Ipsum Dolor",
|
"description": "Lorem Ipsum Dolor",
|
||||||
"project_id": 1,
|
"project_id": 1,
|
||||||
"created_by_id": 1,
|
"created_by_id": 1,
|
||||||
"bucket_id": 1,
|
}, false)
|
||||||
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
|
"task_id": task.ID,
|
||||||
|
"bucket_id": 1,
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
events.AssertDispatched(t, &TaskCreatedEvent{})
|
events.AssertDispatched(t, &TaskCreatedEvent{})
|
||||||
@ -183,8 +184,8 @@ func TestTask_Create(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := task.Create(s, usr)
|
err := task.Create(s, usr)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
"id": task.ID,
|
"task_id": task.ID,
|
||||||
"bucket_id": 22, // default bucket of project 6 but with a position of 2
|
"bucket_id": 22, // default bucket of project 6 but with a position of 2
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
@ -253,12 +254,11 @@ func TestTask_Update(t *testing.T) {
|
|||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
task := &Task{
|
task := &Task{
|
||||||
ID: 4,
|
ID: 4,
|
||||||
Title: "test10000",
|
Title: "test10000",
|
||||||
Description: "Lorem Ipsum Dolor",
|
Description: "Lorem Ipsum Dolor",
|
||||||
KanbanPosition: 10,
|
ProjectID: 1,
|
||||||
ProjectID: 1,
|
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
||||||
BucketID: 2, // Bucket 2 already has 3 tasks and a limit of 3
|
|
||||||
}
|
}
|
||||||
err := task.Update(s, u)
|
err := task.Update(s, u)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -277,7 +277,7 @@ func TestTask_Update(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := task.Update(s, u)
|
err := task.Update(s, u)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.True(t, IsErrBucketDoesNotBelongToProject(err))
|
assert.True(t, IsErrBucketDoesNotExist(err))
|
||||||
})
|
})
|
||||||
t.Run("moving a task to the done bucket", func(t *testing.T) {
|
t.Run("moving a task to the done bucket", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
@ -297,11 +297,12 @@ func TestTask_Update(t *testing.T) {
|
|||||||
assert.True(t, task.Done)
|
assert.True(t, task.Done)
|
||||||
|
|
||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"done": true,
|
"done": true,
|
||||||
"title": "test",
|
}, false)
|
||||||
"project_id": 1,
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
"bucket_id": 3,
|
"task_id": 1,
|
||||||
|
"bucket_id": 3,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
t.Run("moving a repeating task to the done bucket", func(t *testing.T) {
|
t.Run("moving a repeating task to the done bucket", func(t *testing.T) {
|
||||||
@ -321,14 +322,15 @@ func TestTask_Update(t *testing.T) {
|
|||||||
err = s.Commit()
|
err = s.Commit()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, task.Done)
|
assert.False(t, task.Done)
|
||||||
assert.Equal(t, int64(1), task.BucketID) // Bucket should not be updated
|
assert.Equal(t, int64(3), task.BucketID)
|
||||||
|
|
||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||||
"id": 28,
|
"id": 1,
|
||||||
"done": false,
|
"done": false,
|
||||||
"title": "test updated",
|
}, false)
|
||||||
"project_id": 1,
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
"bucket_id": 1,
|
"task_id": 1,
|
||||||
|
"bucket_id": 1,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
t.Run("default bucket when moving a task between projects", func(t *testing.T) {
|
t.Run("default bucket when moving a task between projects", func(t *testing.T) {
|
||||||
@ -345,7 +347,11 @@ func TestTask_Update(t *testing.T) {
|
|||||||
err = s.Commit()
|
err = s.Commit()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, int64(40), task.BucketID) // bucket 40 is the default bucket on project 2
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
|
"task_id": task.ID,
|
||||||
|
// bucket 40 is the default bucket on project 2
|
||||||
|
"bucket_id": 40,
|
||||||
|
}, false)
|
||||||
})
|
})
|
||||||
t.Run("marking a task as done should move it to the done bucket", func(t *testing.T) {
|
t.Run("marking a task as done should move it to the done bucket", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
@ -361,11 +367,13 @@ func TestTask_Update(t *testing.T) {
|
|||||||
err = s.Commit()
|
err = s.Commit()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, task.Done)
|
assert.True(t, task.Done)
|
||||||
assert.Equal(t, int64(3), task.BucketID)
|
|
||||||
|
|
||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"done": true,
|
"done": true,
|
||||||
|
}, false)
|
||||||
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
|
"task_id": 1,
|
||||||
"bucket_id": 3,
|
"bucket_id": 3,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
@ -386,7 +394,10 @@ func TestTask_Update(t *testing.T) {
|
|||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"project_id": 2,
|
"project_id": 2,
|
||||||
"bucket_id": 40,
|
}, false)
|
||||||
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
|
"task_id": 1,
|
||||||
|
"bucket_id": 40,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
t.Run("move done task to another project with a done bucket", func(t *testing.T) {
|
t.Run("move done task to another project with a done bucket", func(t *testing.T) {
|
||||||
@ -405,11 +416,14 @@ func TestTask_Update(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||||
"id": 2,
|
"id": task.ID,
|
||||||
"project_id": 2,
|
"project_id": 2,
|
||||||
"bucket_id": 4, // 4 is the done bucket
|
|
||||||
"done": true,
|
"done": true,
|
||||||
}, false)
|
}, false)
|
||||||
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
|
"task_id": task.ID,
|
||||||
|
"bucket_id": 4, // 4 is the done bucket
|
||||||
|
}, false)
|
||||||
})
|
})
|
||||||
t.Run("repeating tasks should not be moved to the done bucket", func(t *testing.T) {
|
t.Run("repeating tasks should not be moved to the done bucket", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
@ -426,11 +440,13 @@ func TestTask_Update(t *testing.T) {
|
|||||||
err = s.Commit()
|
err = s.Commit()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, task.Done)
|
assert.False(t, task.Done)
|
||||||
assert.Equal(t, int64(1), task.BucketID)
|
|
||||||
|
|
||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||||
"id": 28,
|
"id": 28,
|
||||||
"done": false,
|
"done": false,
|
||||||
|
}, false)
|
||||||
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
|
"task_id": 28,
|
||||||
"bucket_id": 1,
|
"bucket_id": 1,
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
|
@ -19,6 +19,7 @@ package models
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/config"
|
"code.vikunja.io/api/pkg/config"
|
||||||
@ -154,11 +155,11 @@ func CreateTypesenseCollections() error {
|
|||||||
Type: "float",
|
Type: "float",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "kanban_position",
|
Name: "created_by_id",
|
||||||
Type: "float",
|
Type: "int64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "created_by_id",
|
Name: "project_view_id",
|
||||||
Type: "int64",
|
Type: "int64",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -248,7 +249,13 @@ func ReindexAllTasks() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTypesenseTaskForTask(s *xorm.Session, task *Task, projectsCache map[int64]*Project) (ttask *typesenseTask, err error) {
|
func getTypesenseTaskForTask(s *xorm.Session, task *Task, projectsCache map[int64]*Project) (ttask *typesenseTask, err error) {
|
||||||
ttask = convertTaskToTypesenseTask(task)
|
positions := []*TaskPosition{}
|
||||||
|
err = s.Where("task_id = ?", task.ID).Find(&positions)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ttask = convertTaskToTypesenseTask(task, positions)
|
||||||
|
|
||||||
var p *Project
|
var p *Project
|
||||||
if projectsCache == nil {
|
if projectsCache == nil {
|
||||||
@ -284,14 +291,14 @@ func reindexTasksInTypesense(s *xorm.Session, tasks map[int64]*Task) (err error)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = addMoreInfoToTasks(s, tasks, &user.User{ID: 1})
|
err = addMoreInfoToTasks(s, tasks, &user.User{ID: 1}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not fetch more task info: %s", err.Error())
|
return fmt.Errorf("could not fetch more task info: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
projects := make(map[int64]*Project)
|
projects := make(map[int64]*Project)
|
||||||
|
|
||||||
typesenseTasks := []interface{}{}
|
typesenseTasks := []interface{}{}
|
||||||
|
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
|
|
||||||
ttask, err := getTypesenseTaskForTask(s, task, projects)
|
ttask, err := getTypesenseTaskForTask(s, task, projects)
|
||||||
@ -415,19 +422,18 @@ type typesenseTask struct {
|
|||||||
CoverImageAttachmentID int64 `json:"cover_image_attachment_id"`
|
CoverImageAttachmentID int64 `json:"cover_image_attachment_id"`
|
||||||
Created int64 `json:"created"`
|
Created int64 `json:"created"`
|
||||||
Updated int64 `json:"updated"`
|
Updated int64 `json:"updated"`
|
||||||
BucketID int64 `json:"bucket_id"`
|
|
||||||
Position float64 `json:"position"`
|
|
||||||
KanbanPosition float64 `json:"kanban_position"`
|
|
||||||
CreatedByID int64 `json:"created_by_id"`
|
CreatedByID int64 `json:"created_by_id"`
|
||||||
Reminders interface{} `json:"reminders"`
|
Reminders interface{} `json:"reminders"`
|
||||||
Assignees interface{} `json:"assignees"`
|
Assignees interface{} `json:"assignees"`
|
||||||
Labels interface{} `json:"labels"`
|
Labels interface{} `json:"labels"`
|
||||||
//RelatedTasks interface{} `json:"related_tasks"` // TODO
|
//RelatedTasks interface{} `json:"related_tasks"` // TODO
|
||||||
Attachments interface{} `json:"attachments"`
|
Attachments interface{} `json:"attachments"`
|
||||||
Comments interface{} `json:"comments"`
|
Comments interface{} `json:"comments"`
|
||||||
|
Positions map[string]float64 `json:"positions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertTaskToTypesenseTask(task *Task) *typesenseTask {
|
func convertTaskToTypesenseTask(task *Task, positions []*TaskPosition) *typesenseTask {
|
||||||
|
|
||||||
tt := &typesenseTask{
|
tt := &typesenseTask{
|
||||||
ID: fmt.Sprintf("%d", task.ID),
|
ID: fmt.Sprintf("%d", task.ID),
|
||||||
Title: task.Title,
|
Title: task.Title,
|
||||||
@ -449,9 +455,6 @@ func convertTaskToTypesenseTask(task *Task) *typesenseTask {
|
|||||||
CoverImageAttachmentID: task.CoverImageAttachmentID,
|
CoverImageAttachmentID: task.CoverImageAttachmentID,
|
||||||
Created: task.Created.UTC().Unix(),
|
Created: task.Created.UTC().Unix(),
|
||||||
Updated: task.Updated.UTC().Unix(),
|
Updated: task.Updated.UTC().Unix(),
|
||||||
BucketID: task.BucketID,
|
|
||||||
Position: task.Position,
|
|
||||||
KanbanPosition: task.KanbanPosition,
|
|
||||||
CreatedByID: task.CreatedByID,
|
CreatedByID: task.CreatedByID,
|
||||||
Reminders: task.Reminders,
|
Reminders: task.Reminders,
|
||||||
Assignees: task.Assignees,
|
Assignees: task.Assignees,
|
||||||
@ -473,6 +476,10 @@ func convertTaskToTypesenseTask(task *Task) *typesenseTask {
|
|||||||
tt.EndDate = nil
|
tt.EndDate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, position := range positions {
|
||||||
|
tt.Positions["view_"+strconv.FormatInt(position.ProjectViewID, 10)] = position.Position
|
||||||
|
}
|
||||||
|
|
||||||
return tt
|
return tt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +66,9 @@ func SetupTests() {
|
|||||||
"favorites",
|
"favorites",
|
||||||
"api_tokens",
|
"api_tokens",
|
||||||
"reactions",
|
"reactions",
|
||||||
|
"project_views",
|
||||||
|
"task_positions",
|
||||||
|
"task_buckets",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -126,6 +126,7 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||||||
originalBuckets := project.Buckets
|
originalBuckets := project.Buckets
|
||||||
originalBackgroundInformation := project.BackgroundInformation
|
originalBackgroundInformation := project.BackgroundInformation
|
||||||
needsDefaultBucket := false
|
needsDefaultBucket := false
|
||||||
|
oldViews := project.Views
|
||||||
|
|
||||||
// Saving the archived status to archive the project again after creating it
|
// Saving the archived status to archive the project again after creating it
|
||||||
var wasArchived bool
|
var wasArchived bool
|
||||||
@ -182,6 +183,47 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||||||
log.Debugf("[creating structure] Created bucket %d, old ID was %d", bucket.ID, oldID)
|
log.Debugf("[creating structure] Created bucket %d, old ID was %d", bucket.ID, oldID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create all views, create default views if we don't have any
|
||||||
|
if len(oldViews) > 0 {
|
||||||
|
for _, view := range oldViews {
|
||||||
|
view.ID = 0
|
||||||
|
|
||||||
|
if view.DefaultBucketID != 0 {
|
||||||
|
bucket, has := buckets[view.DefaultBucketID]
|
||||||
|
if has {
|
||||||
|
view.DefaultBucketID = bucket.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.DoneBucketID != 0 {
|
||||||
|
bucket, has := buckets[view.DoneBucketID]
|
||||||
|
if has {
|
||||||
|
view.DoneBucketID = bucket.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = view.Create(s, user)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only using the default views
|
||||||
|
// Add all buckets to the default kanban view
|
||||||
|
for _, view := range project.Views {
|
||||||
|
if view.ViewKind == models.ProjectViewKindKanban {
|
||||||
|
for _, b := range buckets {
|
||||||
|
b.ProjectViewID = view.ID
|
||||||
|
err = b.Update(s, user)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("[creating structure] Creating %d tasks", len(tasks))
|
log.Debugf("[creating structure] Creating %d tasks", len(tasks))
|
||||||
|
|
||||||
setBucketOrDefault := func(task *models.Task) {
|
setBucketOrDefault := func(task *models.Task) {
|
||||||
@ -205,7 +247,6 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||||||
oldid := t.ID
|
oldid := t.ID
|
||||||
t.ProjectID = project.ID
|
t.ProjectID = project.ID
|
||||||
err = t.Create(s, user)
|
err = t.Create(s, user)
|
||||||
|
|
||||||
if err != nil && models.IsErrTaskCannotBeEmpty(err) {
|
if err != nil && models.IsErrTaskCannotBeEmpty(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -332,6 +373,14 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||||||
// All tasks brought their own bucket with them, therefore the newly created default bucket is just extra space
|
// All tasks brought their own bucket with them, therefore the newly created default bucket is just extra space
|
||||||
if !needsDefaultBucket {
|
if !needsDefaultBucket {
|
||||||
b := &models.Bucket{ProjectID: project.ID}
|
b := &models.Bucket{ProjectID: project.ID}
|
||||||
|
|
||||||
|
for _, view := range project.Views {
|
||||||
|
if view.ViewKind == models.ProjectViewKindKanban {
|
||||||
|
b.ProjectViewID = view.ID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bucketsIn, _, _, err := b.ReadAll(s, user, "", 1, 1)
|
bucketsIn, _, _, err := b.ReadAll(s, user, "", 1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -341,6 +390,7 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||||||
for _, b := range buckets {
|
for _, b := range buckets {
|
||||||
if b.Title == "Backlog" {
|
if b.Title == "Backlog" {
|
||||||
newBacklogBucket = b
|
newBacklogBucket = b
|
||||||
|
newBacklogBucket.ProjectID = project.ID
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,12 +142,11 @@ func TestInsertFromStructure(t *testing.T) {
|
|||||||
"title": testStructure[1].Title,
|
"title": testStructure[1].Title,
|
||||||
"description": testStructure[1].Description,
|
"description": testStructure[1].Description,
|
||||||
}, false)
|
}, false)
|
||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "task_buckets", map[string]interface{}{
|
||||||
"title": testStructure[1].Tasks[5].Title,
|
"task_id": testStructure[1].Tasks[5].ID,
|
||||||
"bucket_id": testStructure[1].Buckets[0].ID,
|
"bucket_id": testStructure[1].Buckets[0].ID,
|
||||||
}, false)
|
}, false)
|
||||||
db.AssertMissing(t, "tasks", map[string]interface{}{
|
db.AssertMissing(t, "task_buckets", map[string]interface{}{
|
||||||
"title": testStructure[1].Tasks[6].Title,
|
|
||||||
"bucket_id": 1111, // No task with that bucket should exist
|
"bucket_id": 1111, // No task with that bucket should exist
|
||||||
})
|
})
|
||||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||||
|
@ -253,9 +253,8 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV
|
|||||||
|
|
||||||
// The usual stuff: Title, description, position, bucket id
|
// The usual stuff: Title, description, position, bucket id
|
||||||
task := &models.Task{
|
task := &models.Task{
|
||||||
Title: card.Name,
|
Title: card.Name,
|
||||||
KanbanPosition: card.Pos,
|
BucketID: bucketID,
|
||||||
BucketID: bucketID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task.Description, err = convertMarkdownToHTML(card.Desc)
|
task.Description, err = convertMarkdownToHTML(card.Desc)
|
||||||
|
@ -228,11 +228,10 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||||||
Tasks: []*models.TaskWithComments{
|
Tasks: []*models.TaskWithComments{
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 1",
|
Title: "Test Card 1",
|
||||||
Description: "<p>Card Description <strong>bold</strong></p>\n",
|
Description: "<p>Card Description <strong>bold</strong></p>\n",
|
||||||
BucketID: 1,
|
BucketID: 1,
|
||||||
KanbanPosition: 123,
|
DueDate: time1,
|
||||||
DueDate: time1,
|
|
||||||
Labels: []*models.Label{
|
Labels: []*models.Label{
|
||||||
{
|
{
|
||||||
Title: "Label 1",
|
Title: "Label 1",
|
||||||
@ -271,22 +270,19 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||||||
<ul data-type="taskList">
|
<ul data-type="taskList">
|
||||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>Pending Task</p></div></li>
|
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>Pending Task</p></div></li>
|
||||||
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>Another Pending Task</p></div></li></ul>`,
|
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>Another Pending Task</p></div></li></ul>`,
|
||||||
BucketID: 1,
|
BucketID: 1,
|
||||||
KanbanPosition: 124,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 3",
|
Title: "Test Card 3",
|
||||||
BucketID: 1,
|
BucketID: 1,
|
||||||
KanbanPosition: 126,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 4",
|
Title: "Test Card 4",
|
||||||
BucketID: 1,
|
BucketID: 1,
|
||||||
KanbanPosition: 127,
|
|
||||||
Labels: []*models.Label{
|
Labels: []*models.Label{
|
||||||
{
|
{
|
||||||
Title: "Label 2",
|
Title: "Label 2",
|
||||||
@ -297,9 +293,8 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 5",
|
Title: "Test Card 5",
|
||||||
BucketID: 2,
|
BucketID: 2,
|
||||||
KanbanPosition: 111,
|
|
||||||
Labels: []*models.Label{
|
Labels: []*models.Label{
|
||||||
{
|
{
|
||||||
Title: "Label 3",
|
Title: "Label 3",
|
||||||
@ -318,24 +313,21 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 6",
|
Title: "Test Card 6",
|
||||||
BucketID: 2,
|
BucketID: 2,
|
||||||
KanbanPosition: 222,
|
DueDate: time1,
|
||||||
DueDate: time1,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 7",
|
Title: "Test Card 7",
|
||||||
BucketID: 2,
|
BucketID: 2,
|
||||||
KanbanPosition: 333,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 8",
|
Title: "Test Card 8",
|
||||||
BucketID: 2,
|
BucketID: 2,
|
||||||
KanbanPosition: 444,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -355,9 +347,8 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||||||
Tasks: []*models.TaskWithComments{
|
Tasks: []*models.TaskWithComments{
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 634",
|
Title: "Test Card 634",
|
||||||
BucketID: 3,
|
BucketID: 3,
|
||||||
KanbanPosition: 123,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -378,9 +369,8 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||||||
Tasks: []*models.TaskWithComments{
|
Tasks: []*models.TaskWithComments{
|
||||||
{
|
{
|
||||||
Task: models.Task{
|
Task: models.Task{
|
||||||
Title: "Test Card 63423",
|
Title: "Test Card 63423",
|
||||||
BucketID: 4,
|
BucketID: 4,
|
||||||
KanbanPosition: 123,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -355,6 +355,7 @@ func registerAPIRoutes(a *echo.Group) {
|
|||||||
return &models.TaskCollection{}
|
return &models.TaskCollection{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
a.GET("/projects/:project/views/:view/tasks", taskCollectionHandler.ReadAllWeb)
|
||||||
a.GET("/projects/:project/tasks", taskCollectionHandler.ReadAllWeb)
|
a.GET("/projects/:project/tasks", taskCollectionHandler.ReadAllWeb)
|
||||||
|
|
||||||
kanbanBucketHandler := &handler.WebHandler{
|
kanbanBucketHandler := &handler.WebHandler{
|
||||||
@ -362,10 +363,10 @@ func registerAPIRoutes(a *echo.Group) {
|
|||||||
return &models.Bucket{}
|
return &models.Bucket{}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
a.GET("/projects/:project/buckets", kanbanBucketHandler.ReadAllWeb)
|
a.GET("/projects/:project/views/:view/buckets", kanbanBucketHandler.ReadAllWeb)
|
||||||
a.PUT("/projects/:project/buckets", kanbanBucketHandler.CreateWeb)
|
a.PUT("/projects/:project/views/:view/buckets", kanbanBucketHandler.CreateWeb)
|
||||||
a.POST("/projects/:project/buckets/:bucket", kanbanBucketHandler.UpdateWeb)
|
a.POST("/projects/:project/views/:view/buckets/:bucket", kanbanBucketHandler.UpdateWeb)
|
||||||
a.DELETE("/projects/:project/buckets/:bucket", kanbanBucketHandler.DeleteWeb)
|
a.DELETE("/projects/:project/views/:view/buckets/:bucket", kanbanBucketHandler.DeleteWeb)
|
||||||
|
|
||||||
projectDuplicateHandler := &handler.WebHandler{
|
projectDuplicateHandler := &handler.WebHandler{
|
||||||
EmptyStruct: func() handler.CObject {
|
EmptyStruct: func() handler.CObject {
|
||||||
@ -385,6 +386,13 @@ func registerAPIRoutes(a *echo.Group) {
|
|||||||
a.DELETE("/tasks/:projecttask", taskHandler.DeleteWeb)
|
a.DELETE("/tasks/:projecttask", taskHandler.DeleteWeb)
|
||||||
a.POST("/tasks/:projecttask", taskHandler.UpdateWeb)
|
a.POST("/tasks/:projecttask", taskHandler.UpdateWeb)
|
||||||
|
|
||||||
|
taskPositionHandler := &handler.WebHandler{
|
||||||
|
EmptyStruct: func() handler.CObject {
|
||||||
|
return &models.TaskPosition{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.POST("/tasks/:task/position", taskPositionHandler.UpdateWeb)
|
||||||
|
|
||||||
bulkTaskHandler := &handler.WebHandler{
|
bulkTaskHandler := &handler.WebHandler{
|
||||||
EmptyStruct: func() handler.CObject {
|
EmptyStruct: func() handler.CObject {
|
||||||
return &models.BulkTask{}
|
return &models.BulkTask{}
|
||||||
@ -590,6 +598,7 @@ func registerAPIRoutes(a *echo.Group) {
|
|||||||
a.GET("/webhooks/events", apiv1.GetAvailableWebhookEvents)
|
a.GET("/webhooks/events", apiv1.GetAvailableWebhookEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reactions
|
||||||
reactionProvider := &handler.WebHandler{
|
reactionProvider := &handler.WebHandler{
|
||||||
EmptyStruct: func() handler.CObject {
|
EmptyStruct: func() handler.CObject {
|
||||||
return &models.Reaction{}
|
return &models.Reaction{}
|
||||||
@ -598,6 +607,19 @@ func registerAPIRoutes(a *echo.Group) {
|
|||||||
a.GET("/:entitykind/:entityid/reactions", reactionProvider.ReadAllWeb)
|
a.GET("/:entitykind/:entityid/reactions", reactionProvider.ReadAllWeb)
|
||||||
a.POST("/:entitykind/:entityid/reactions/delete", reactionProvider.DeleteWeb)
|
a.POST("/:entitykind/:entityid/reactions/delete", reactionProvider.DeleteWeb)
|
||||||
a.PUT("/:entitykind/:entityid/reactions", reactionProvider.CreateWeb)
|
a.PUT("/:entitykind/:entityid/reactions", reactionProvider.CreateWeb)
|
||||||
|
|
||||||
|
// Project views
|
||||||
|
projectViewProvider := &handler.WebHandler{
|
||||||
|
EmptyStruct: func() handler.CObject {
|
||||||
|
return &models.ProjectView{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
a.GET("/projects/:project/views", projectViewProvider.ReadAllWeb)
|
||||||
|
a.GET("/projects/:project/views/:view", projectViewProvider.ReadOneWeb)
|
||||||
|
a.PUT("/projects/:project/views", projectViewProvider.CreateWeb)
|
||||||
|
a.DELETE("/projects/:project/views/:view", projectViewProvider.DeleteWeb)
|
||||||
|
a.POST("/projects/:project/views/:view", projectViewProvider.UpdateWeb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerMigrations(m *echo.Group) {
|
func registerMigrations(m *echo.Group) {
|
||||||
|
1215
pkg/swagger/docs.go
1215
pkg/swagger/docs.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -123,8 +123,8 @@ definitions:
|
|||||||
description: The position this bucket has when querying all buckets. See the
|
description: The position this bucket has when querying all buckets. See the
|
||||||
tasks.position property on how to use this.
|
tasks.position property on how to use this.
|
||||||
type: number
|
type: number
|
||||||
project_id:
|
project_view_id:
|
||||||
description: The project this bucket belongs to.
|
description: The project view this bucket belongs to.
|
||||||
type: integer
|
type: integer
|
||||||
tasks:
|
tasks:
|
||||||
description: All tasks which belong to this bucket.
|
description: All tasks which belong to this bucket.
|
||||||
@ -140,6 +140,16 @@ definitions:
|
|||||||
this value.
|
this value.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
models.BucketConfigurationModeKind:
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
type: integer
|
||||||
|
x-enum-varnames:
|
||||||
|
- BucketConfigurationModeNone
|
||||||
|
- BucketConfigurationModeManual
|
||||||
|
- BucketConfigurationModeFilter
|
||||||
models.BulkAssignees:
|
models.BulkAssignees:
|
||||||
properties:
|
properties:
|
||||||
assignees:
|
assignees:
|
||||||
@ -161,7 +171,9 @@ definitions:
|
|||||||
$ref: '#/definitions/models.TaskAttachment'
|
$ref: '#/definitions/models.TaskAttachment'
|
||||||
type: array
|
type: array
|
||||||
bucket_id:
|
bucket_id:
|
||||||
description: BucketID is the ID of the kanban bucket this task belongs to.
|
description: |-
|
||||||
|
The bucket id. Will only be populated when the task is accessed via a view with buckets.
|
||||||
|
Can be used to move a task between buckets. In that case, the new bucket must be in the same view as the old one.
|
||||||
type: integer
|
type: integer
|
||||||
cover_image_attachment_id:
|
cover_image_attachment_id:
|
||||||
description: If this task has a cover image, the field will return the id
|
description: If this task has a cover image, the field will return the id
|
||||||
@ -209,10 +221,6 @@ definitions:
|
|||||||
a separate "Important" project. This value depends on the user making the
|
a separate "Important" project. This value depends on the user making the
|
||||||
call to the api.
|
call to the api.
|
||||||
type: boolean
|
type: boolean
|
||||||
kanban_position:
|
|
||||||
description: The position of tasks in the kanban board. See the docs for the
|
|
||||||
`position` property on how to use this.
|
|
||||||
type: number
|
|
||||||
labels:
|
labels:
|
||||||
description: An array of labels which are associated with this task.
|
description: An array of labels which are associated with this task.
|
||||||
items:
|
items:
|
||||||
@ -224,11 +232,9 @@ definitions:
|
|||||||
position:
|
position:
|
||||||
description: |-
|
description: |-
|
||||||
The position of the task - any task project can be sorted as usual by this parameter.
|
The position of the task - any task project can be sorted as usual by this parameter.
|
||||||
When accessing tasks via kanban buckets, this is primarily used to sort them based on a range
|
When accessing tasks via views with buckets, this is primarily used to sort them based on a range.
|
||||||
We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number).
|
Positions are always saved per view. They will automatically be set if you request the tasks through a view
|
||||||
You would calculate the new position between two tasks with something like task3.position = (task2.position - task1.position) / 2.
|
endpoint, otherwise they will always be 0. To update them, take a look at the Task Position endpoint.
|
||||||
A 64-Bit float leaves plenty of room to initially give tasks a position with 2^16 difference to the previous task
|
|
||||||
which also leaves a lot of room for rearranging and sorting later.
|
|
||||||
type: number
|
type: number
|
||||||
priority:
|
priority:
|
||||||
description: The task priority. Can be anything you want, it is possible to
|
description: The task priority. Can be anything you want, it is possible to
|
||||||
@ -422,16 +428,13 @@ definitions:
|
|||||||
description: A timestamp when this project was created. You cannot change
|
description: A timestamp when this project was created. You cannot change
|
||||||
this value.
|
this value.
|
||||||
type: string
|
type: string
|
||||||
default_bucket_id:
|
|
||||||
description: The ID of the bucket where new tasks without a bucket are added
|
|
||||||
to. By default, this is the leftmost bucket in a project.
|
|
||||||
type: integer
|
|
||||||
description:
|
description:
|
||||||
description: The description of the project.
|
description: The description of the project.
|
||||||
type: string
|
type: string
|
||||||
done_bucket_id:
|
done_bucket_id:
|
||||||
description: If tasks are moved to the done bucket, they are marked as done.
|
description: 'Deprecated: If tasks are moved to the done bucket, they are
|
||||||
If they are marked as done individually, they are moved into the done bucket.
|
marked as done. If they are marked as done individually, they are moved
|
||||||
|
into the done bucket.'
|
||||||
type: integer
|
type: integer
|
||||||
hex_color:
|
hex_color:
|
||||||
description: The hex color of this project
|
description: The hex color of this project
|
||||||
@ -478,6 +481,10 @@ definitions:
|
|||||||
description: A timestamp when this project was last updated. You cannot change
|
description: A timestamp when this project was last updated. You cannot change
|
||||||
this value.
|
this value.
|
||||||
type: string
|
type: string
|
||||||
|
views:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.ProjectView'
|
||||||
|
type: array
|
||||||
type: object
|
type: object
|
||||||
models.ProjectDuplicate:
|
models.ProjectDuplicate:
|
||||||
properties:
|
properties:
|
||||||
@ -513,6 +520,77 @@ definitions:
|
|||||||
description: The username.
|
description: The username.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
models.ProjectView:
|
||||||
|
properties:
|
||||||
|
bucket_configuration:
|
||||||
|
description: When the bucket configuration mode is not `manual`, this field
|
||||||
|
holds the options of that configuration.
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.ProjectViewBucketConfiguration'
|
||||||
|
type: array
|
||||||
|
bucket_configuration_mode:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/models.BucketConfigurationModeKind'
|
||||||
|
description: The bucket configuration mode. Can be `none`, `manual` or `filter`.
|
||||||
|
`manual` allows to move tasks between buckets as you normally would. `filter`
|
||||||
|
creates buckets based on a filter for each bucket.
|
||||||
|
created:
|
||||||
|
description: A timestamp when this reaction was created. You cannot change
|
||||||
|
this value.
|
||||||
|
type: string
|
||||||
|
default_bucket_id:
|
||||||
|
description: The ID of the bucket where new tasks without a bucket are added
|
||||||
|
to. By default, this is the leftmost bucket in a view.
|
||||||
|
type: integer
|
||||||
|
done_bucket_id:
|
||||||
|
description: If tasks are moved to the done bucket, they are marked as done.
|
||||||
|
If they are marked as done individually, they are moved into the done bucket.
|
||||||
|
type: integer
|
||||||
|
filter:
|
||||||
|
description: The filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
||||||
|
for a full explanation.
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
description: The unique numeric id of this view
|
||||||
|
type: integer
|
||||||
|
position:
|
||||||
|
description: The position of this view in the list. The list of all views
|
||||||
|
will be sorted by this parameter.
|
||||||
|
type: number
|
||||||
|
project_id:
|
||||||
|
description: The project this view belongs to
|
||||||
|
type: integer
|
||||||
|
title:
|
||||||
|
description: The title of this view
|
||||||
|
type: string
|
||||||
|
updated:
|
||||||
|
description: A timestamp when this view was updated. You cannot change this
|
||||||
|
value.
|
||||||
|
type: string
|
||||||
|
view_kind:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/models.ProjectViewKind'
|
||||||
|
description: The kind of this view. Can be `list`, `gantt`, `table` or `kanban`.
|
||||||
|
type: object
|
||||||
|
models.ProjectViewBucketConfiguration:
|
||||||
|
properties:
|
||||||
|
filter:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
models.ProjectViewKind:
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
type: integer
|
||||||
|
x-enum-varnames:
|
||||||
|
- ProjectViewKindList
|
||||||
|
- ProjectViewKindGantt
|
||||||
|
- ProjectViewKindTable
|
||||||
|
- ProjectViewKindKanban
|
||||||
models.Reaction:
|
models.Reaction:
|
||||||
properties:
|
properties:
|
||||||
created:
|
created:
|
||||||
@ -671,7 +749,9 @@ definitions:
|
|||||||
$ref: '#/definitions/models.TaskAttachment'
|
$ref: '#/definitions/models.TaskAttachment'
|
||||||
type: array
|
type: array
|
||||||
bucket_id:
|
bucket_id:
|
||||||
description: BucketID is the ID of the kanban bucket this task belongs to.
|
description: |-
|
||||||
|
The bucket id. Will only be populated when the task is accessed via a view with buckets.
|
||||||
|
Can be used to move a task between buckets. In that case, the new bucket must be in the same view as the old one.
|
||||||
type: integer
|
type: integer
|
||||||
cover_image_attachment_id:
|
cover_image_attachment_id:
|
||||||
description: If this task has a cover image, the field will return the id
|
description: If this task has a cover image, the field will return the id
|
||||||
@ -719,10 +799,6 @@ definitions:
|
|||||||
a separate "Important" project. This value depends on the user making the
|
a separate "Important" project. This value depends on the user making the
|
||||||
call to the api.
|
call to the api.
|
||||||
type: boolean
|
type: boolean
|
||||||
kanban_position:
|
|
||||||
description: The position of tasks in the kanban board. See the docs for the
|
|
||||||
`position` property on how to use this.
|
|
||||||
type: number
|
|
||||||
labels:
|
labels:
|
||||||
description: An array of labels which are associated with this task.
|
description: An array of labels which are associated with this task.
|
||||||
items:
|
items:
|
||||||
@ -734,11 +810,9 @@ definitions:
|
|||||||
position:
|
position:
|
||||||
description: |-
|
description: |-
|
||||||
The position of the task - any task project can be sorted as usual by this parameter.
|
The position of the task - any task project can be sorted as usual by this parameter.
|
||||||
When accessing tasks via kanban buckets, this is primarily used to sort them based on a range
|
When accessing tasks via views with buckets, this is primarily used to sort them based on a range.
|
||||||
We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number).
|
Positions are always saved per view. They will automatically be set if you request the tasks through a view
|
||||||
You would calculate the new position between two tasks with something like task3.position = (task2.position - task1.position) / 2.
|
endpoint, otherwise they will always be 0. To update them, take a look at the Task Position endpoint.
|
||||||
A 64-Bit float leaves plenty of room to initially give tasks a position with 2^16 difference to the previous task
|
|
||||||
which also leaves a lot of room for rearranging and sorting later.
|
|
||||||
type: number
|
type: number
|
||||||
priority:
|
priority:
|
||||||
description: The task priority. Can be anything you want, it is possible to
|
description: The task priority. Can be anything you want, it is possible to
|
||||||
@ -814,7 +888,7 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
filter:
|
filter:
|
||||||
description: The filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
description: The filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
||||||
for a full explanation of the feature.
|
for a full explanation.
|
||||||
type: string
|
type: string
|
||||||
filter_include_nulls:
|
filter_include_nulls:
|
||||||
description: If set to true, the result will also include null values
|
description: If set to true, the result will also include null values
|
||||||
@ -847,6 +921,26 @@ definitions:
|
|||||||
updated:
|
updated:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
models.TaskPosition:
|
||||||
|
properties:
|
||||||
|
position:
|
||||||
|
description: |-
|
||||||
|
The position of the task - any task project can be sorted as usual by this parameter.
|
||||||
|
When accessing tasks via kanban buckets, this is primarily used to sort them based on a range
|
||||||
|
We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number).
|
||||||
|
You would calculate the new position between two tasks with something like task3.position = (task2.position - task1.position) / 2.
|
||||||
|
A 64-Bit float leaves plenty of room to initially give tasks a position with 2^16 difference to the previous task
|
||||||
|
which also leaves a lot of room for rearranging and sorting later.
|
||||||
|
Positions are always saved per view. They will automatically be set if you request the tasks through a view
|
||||||
|
endpoint, otherwise they will always be 0. To update them, take a look at the Task Position endpoint.
|
||||||
|
type: number
|
||||||
|
project_view_id:
|
||||||
|
description: The project view this task is related to
|
||||||
|
type: integer
|
||||||
|
task_id:
|
||||||
|
description: The ID of the task this position is for
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
models.TaskRelation:
|
models.TaskRelation:
|
||||||
properties:
|
properties:
|
||||||
created:
|
created:
|
||||||
@ -2850,107 +2944,6 @@ paths:
|
|||||||
summary: Upload a project background
|
summary: Upload a project background
|
||||||
tags:
|
tags:
|
||||||
- project
|
- project
|
||||||
/projects/{id}/buckets:
|
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Returns all kanban buckets with belong to a project including their
|
|
||||||
tasks. Buckets are always sorted by their `position` in ascending order. Tasks
|
|
||||||
are sorted by their `kanban_position` in ascending order.
|
|
||||||
parameters:
|
|
||||||
- description: Project Id
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: The page number for tasks. Used for pagination. If not provided,
|
|
||||||
the first page of results is returned.
|
|
||||||
in: query
|
|
||||||
name: page
|
|
||||||
type: integer
|
|
||||||
- description: The maximum number of tasks per bucket per page. This parameter
|
|
||||||
is limited by the configured maximum of items per page.
|
|
||||||
in: query
|
|
||||||
name: per_page
|
|
||||||
type: integer
|
|
||||||
- description: Search tasks by task text.
|
|
||||||
in: query
|
|
||||||
name: s
|
|
||||||
type: string
|
|
||||||
- description: The filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
|
||||||
for a full explanation of the feature.
|
|
||||||
in: query
|
|
||||||
name: filter
|
|
||||||
type: string
|
|
||||||
- description: 'The time zone which should be used for date match (statements
|
|
||||||
like '
|
|
||||||
in: query
|
|
||||||
name: filter_timezone
|
|
||||||
type: string
|
|
||||||
- description: If set to true the result will include filtered fields whose
|
|
||||||
value is set to `null`. Available values are `true` or `false`. Defaults
|
|
||||||
to `false`.
|
|
||||||
in: query
|
|
||||||
name: filter_include_nulls
|
|
||||||
type: string
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: The buckets with their tasks
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/models.Bucket'
|
|
||||||
type: array
|
|
||||||
"500":
|
|
||||||
description: Internal server error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/models.Message'
|
|
||||||
security:
|
|
||||||
- JWTKeyAuth: []
|
|
||||||
summary: Get all kanban buckets of a project
|
|
||||||
tags:
|
|
||||||
- project
|
|
||||||
put:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Creates a new kanban bucket on a project.
|
|
||||||
parameters:
|
|
||||||
- description: Project Id
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: The bucket object
|
|
||||||
in: body
|
|
||||||
name: bucket
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/models.Bucket'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: The created bucket object.
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/models.Bucket'
|
|
||||||
"400":
|
|
||||||
description: Invalid bucket object provided.
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/web.HTTPError'
|
|
||||||
"404":
|
|
||||||
description: The project does not exist.
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/web.HTTPError'
|
|
||||||
"500":
|
|
||||||
description: Internal error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/models.Message'
|
|
||||||
security:
|
|
||||||
- JWTKeyAuth: []
|
|
||||||
summary: Create a new bucket
|
|
||||||
tags:
|
|
||||||
- project
|
|
||||||
/projects/{id}/projectusers:
|
/projects/{id}/projectusers:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@ -2994,78 +2987,6 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- project
|
- project
|
||||||
/projects/{id}/tasks:
|
/projects/{id}/tasks:
|
||||||
get:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Returns all tasks for the current project.
|
|
||||||
parameters:
|
|
||||||
- description: The project ID.
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: The page number. Used for pagination. If not provided, the first
|
|
||||||
page of results is returned.
|
|
||||||
in: query
|
|
||||||
name: page
|
|
||||||
type: integer
|
|
||||||
- description: The maximum number of items per page. Note this parameter is
|
|
||||||
limited by the configured maximum of items per page.
|
|
||||||
in: query
|
|
||||||
name: per_page
|
|
||||||
type: integer
|
|
||||||
- description: Search tasks by task text.
|
|
||||||
in: query
|
|
||||||
name: s
|
|
||||||
type: string
|
|
||||||
- description: The sorting parameter. You can pass this multiple times to get
|
|
||||||
the tasks ordered by multiple different parametes, along with `order_by`.
|
|
||||||
Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`,
|
|
||||||
`due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`,
|
|
||||||
`end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default
|
|
||||||
is `id`.
|
|
||||||
in: query
|
|
||||||
name: sort_by
|
|
||||||
type: string
|
|
||||||
- description: The ordering parameter. Possible values to order by are `asc`
|
|
||||||
or `desc`. Default is `asc`.
|
|
||||||
in: query
|
|
||||||
name: order_by
|
|
||||||
type: string
|
|
||||||
- description: The filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
|
||||||
for a full explanation of the feature.
|
|
||||||
in: query
|
|
||||||
name: filter
|
|
||||||
type: string
|
|
||||||
- description: 'The time zone which should be used for date match (statements
|
|
||||||
like '
|
|
||||||
in: query
|
|
||||||
name: filter_timezone
|
|
||||||
type: string
|
|
||||||
- description: If set to true the result will include filtered fields whose
|
|
||||||
value is set to `null`. Available values are `true` or `false`. Defaults
|
|
||||||
to `false`.
|
|
||||||
in: query
|
|
||||||
name: filter_include_nulls
|
|
||||||
type: string
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: The tasks
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/models.Task'
|
|
||||||
type: array
|
|
||||||
"500":
|
|
||||||
description: Internal error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/models.Message'
|
|
||||||
security:
|
|
||||||
- JWTKeyAuth: []
|
|
||||||
summary: Get tasks in a project
|
|
||||||
tags:
|
|
||||||
- task
|
|
||||||
put:
|
put:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
@ -3288,6 +3209,165 @@ paths:
|
|||||||
summary: Add a user to a project
|
summary: Add a user to a project
|
||||||
tags:
|
tags:
|
||||||
- sharing
|
- sharing
|
||||||
|
/projects/{id}/views/{view}/buckets:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns all kanban buckets which belong to that project. Buckets
|
||||||
|
are always sorted by their `position` in ascending order. To get all buckets
|
||||||
|
with their tasks, use the tasks endpoint with a kanban view.
|
||||||
|
parameters:
|
||||||
|
- description: Project ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Project view ID
|
||||||
|
in: path
|
||||||
|
name: view
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The buckets
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.Bucket'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
security:
|
||||||
|
- JWTKeyAuth: []
|
||||||
|
summary: Get all kanban buckets of a project
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Creates a new kanban bucket on a project.
|
||||||
|
parameters:
|
||||||
|
- description: Project Id
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Project view ID
|
||||||
|
in: path
|
||||||
|
name: view
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The bucket object
|
||||||
|
in: body
|
||||||
|
name: bucket
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Bucket'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The created bucket object.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Bucket'
|
||||||
|
"400":
|
||||||
|
description: Invalid bucket object provided.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/web.HTTPError'
|
||||||
|
"404":
|
||||||
|
description: The project does not exist.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/web.HTTPError'
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
security:
|
||||||
|
- JWTKeyAuth: []
|
||||||
|
summary: Create a new bucket
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
/projects/{id}/views/{view}/tasks:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns all tasks for the current project.
|
||||||
|
parameters:
|
||||||
|
- description: The project ID.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The project view ID.
|
||||||
|
in: path
|
||||||
|
name: view
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The page number. Used for pagination. If not provided, the first
|
||||||
|
page of results is returned.
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
type: integer
|
||||||
|
- description: The maximum number of items per page. Note this parameter is
|
||||||
|
limited by the configured maximum of items per page.
|
||||||
|
in: query
|
||||||
|
name: per_page
|
||||||
|
type: integer
|
||||||
|
- description: Search tasks by task text.
|
||||||
|
in: query
|
||||||
|
name: s
|
||||||
|
type: string
|
||||||
|
- description: The sorting parameter. You can pass this multiple times to get
|
||||||
|
the tasks ordered by multiple different parametes, along with `order_by`.
|
||||||
|
Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`,
|
||||||
|
`due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`,
|
||||||
|
`end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default
|
||||||
|
is `id`.
|
||||||
|
in: query
|
||||||
|
name: sort_by
|
||||||
|
type: string
|
||||||
|
- description: The ordering parameter. Possible values to order by are `asc`
|
||||||
|
or `desc`. Default is `asc`.
|
||||||
|
in: query
|
||||||
|
name: order_by
|
||||||
|
type: string
|
||||||
|
- description: The filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
||||||
|
for a full explanation of the feature.
|
||||||
|
in: query
|
||||||
|
name: filter
|
||||||
|
type: string
|
||||||
|
- description: 'The time zone which should be used for date match (statements
|
||||||
|
like '
|
||||||
|
in: query
|
||||||
|
name: filter_timezone
|
||||||
|
type: string
|
||||||
|
- description: If set to true the result will include filtered fields whose
|
||||||
|
value is set to `null`. Available values are `true` or `false`. Defaults
|
||||||
|
to `false`.
|
||||||
|
in: query
|
||||||
|
name: filter_include_nulls
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The tasks
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.Task'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
security:
|
||||||
|
- JWTKeyAuth: []
|
||||||
|
summary: Get tasks in a project
|
||||||
|
tags:
|
||||||
|
- task
|
||||||
/projects/{id}/webhooks:
|
/projects/{id}/webhooks:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@ -3604,32 +3684,60 @@ paths:
|
|||||||
summary: Get one link shares for a project
|
summary: Get one link shares for a project
|
||||||
tags:
|
tags:
|
||||||
- sharing
|
- sharing
|
||||||
/projects/{projectID}/buckets/{bucketID}:
|
/projects/{project}/views:
|
||||||
delete:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Deletes an existing kanban bucket and dissociates all of its task.
|
description: Returns all project views for a sepcific project
|
||||||
It does not delete any tasks. You cannot delete the last bucket on a project.
|
|
||||||
parameters:
|
parameters:
|
||||||
- description: Project Id
|
- description: Project ID
|
||||||
in: path
|
in: path
|
||||||
name: projectID
|
name: project
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Bucket Id
|
|
||||||
in: path
|
|
||||||
name: bucketID
|
|
||||||
required: true
|
required: true
|
||||||
type: integer
|
type: integer
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Successfully deleted.
|
description: The project views
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.ProjectView'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/models.Message'
|
$ref: '#/definitions/models.Message'
|
||||||
"404":
|
security:
|
||||||
description: The bucket does not exist.
|
- JWTKeyAuth: []
|
||||||
|
summary: Get all project views for a project
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Create a project view in a specific project.
|
||||||
|
parameters:
|
||||||
|
- description: Project ID
|
||||||
|
in: path
|
||||||
|
name: project
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The project view you want to create.
|
||||||
|
in: body
|
||||||
|
name: view
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.ProjectView'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The created project view
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.ProjectView'
|
||||||
|
"403":
|
||||||
|
description: The user does not have access to create a project view
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/web.HTTPError'
|
$ref: '#/definitions/web.HTTPError'
|
||||||
"500":
|
"500":
|
||||||
@ -3638,43 +3746,110 @@ paths:
|
|||||||
$ref: '#/definitions/models.Message'
|
$ref: '#/definitions/models.Message'
|
||||||
security:
|
security:
|
||||||
- JWTKeyAuth: []
|
- JWTKeyAuth: []
|
||||||
summary: Deletes an existing bucket
|
summary: Create a project view
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
/projects/{project}/views/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Deletes a project view.
|
||||||
|
parameters:
|
||||||
|
- description: Project ID
|
||||||
|
in: path
|
||||||
|
name: project
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Project View ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The project view was successfully deleted.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
"403":
|
||||||
|
description: The user does not have access to the project view
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/web.HTTPError'
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
security:
|
||||||
|
- JWTKeyAuth: []
|
||||||
|
summary: Delete a project view
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns a project view by its ID.
|
||||||
|
parameters:
|
||||||
|
- description: Project ID
|
||||||
|
in: path
|
||||||
|
name: project
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Project View ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The project view
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.ProjectView'
|
||||||
|
"403":
|
||||||
|
description: The user does not have access to this project view
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/web.HTTPError'
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
security:
|
||||||
|
- JWTKeyAuth: []
|
||||||
|
summary: Get one project view
|
||||||
tags:
|
tags:
|
||||||
- project
|
- project
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Updates an existing kanban bucket.
|
description: Updates a project view.
|
||||||
parameters:
|
parameters:
|
||||||
- description: Project Id
|
- description: Project ID
|
||||||
in: path
|
in: path
|
||||||
name: projectID
|
name: project
|
||||||
required: true
|
required: true
|
||||||
type: integer
|
type: integer
|
||||||
- description: Bucket Id
|
- description: Project View ID
|
||||||
in: path
|
in: path
|
||||||
name: bucketID
|
name: id
|
||||||
required: true
|
required: true
|
||||||
type: integer
|
type: integer
|
||||||
- description: The bucket object
|
- description: The project view with updated values you want to change.
|
||||||
in: body
|
in: body
|
||||||
name: bucket
|
name: view
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/models.Bucket'
|
$ref: '#/definitions/models.ProjectView'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: The created bucket object.
|
description: The updated project view.
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/models.Bucket'
|
$ref: '#/definitions/models.ProjectView'
|
||||||
"400":
|
"400":
|
||||||
description: Invalid bucket object provided.
|
description: Invalid project view object provided.
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/web.HTTPError'
|
|
||||||
"404":
|
|
||||||
description: The bucket does not exist.
|
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/web.HTTPError'
|
$ref: '#/definitions/web.HTTPError'
|
||||||
"500":
|
"500":
|
||||||
@ -3683,7 +3858,7 @@ paths:
|
|||||||
$ref: '#/definitions/models.Message'
|
$ref: '#/definitions/models.Message'
|
||||||
security:
|
security:
|
||||||
- JWTKeyAuth: []
|
- JWTKeyAuth: []
|
||||||
summary: Update an existing bucket
|
summary: Updates a project view
|
||||||
tags:
|
tags:
|
||||||
- project
|
- project
|
||||||
/projects/{projectID}/duplicate:
|
/projects/{projectID}/duplicate:
|
||||||
@ -3900,6 +4075,98 @@ paths:
|
|||||||
summary: Update a user <-> project relation
|
summary: Update a user <-> project relation
|
||||||
tags:
|
tags:
|
||||||
- sharing
|
- sharing
|
||||||
|
/projects/{projectID}/views/{view}/buckets/{bucketID}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Deletes an existing kanban bucket and dissociates all of its task.
|
||||||
|
It does not delete any tasks. You cannot delete the last bucket on a project.
|
||||||
|
parameters:
|
||||||
|
- description: Project Id
|
||||||
|
in: path
|
||||||
|
name: projectID
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Bucket Id
|
||||||
|
in: path
|
||||||
|
name: bucketID
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Project view ID
|
||||||
|
in: path
|
||||||
|
name: view
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successfully deleted.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
"404":
|
||||||
|
description: The bucket does not exist.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/web.HTTPError'
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
security:
|
||||||
|
- JWTKeyAuth: []
|
||||||
|
summary: Deletes an existing bucket
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Updates an existing kanban bucket.
|
||||||
|
parameters:
|
||||||
|
- description: Project Id
|
||||||
|
in: path
|
||||||
|
name: projectID
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Bucket Id
|
||||||
|
in: path
|
||||||
|
name: bucketID
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Project view ID
|
||||||
|
in: path
|
||||||
|
name: view
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The bucket object
|
||||||
|
in: body
|
||||||
|
name: bucket
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Bucket'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The created bucket object.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Bucket'
|
||||||
|
"400":
|
||||||
|
description: Invalid bucket object provided.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/web.HTTPError'
|
||||||
|
"404":
|
||||||
|
description: The bucket does not exist.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/web.HTTPError'
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
security:
|
||||||
|
- JWTKeyAuth: []
|
||||||
|
summary: Update an existing bucket
|
||||||
|
tags:
|
||||||
|
- project
|
||||||
/register:
|
/register:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@ -4334,6 +4601,43 @@ paths:
|
|||||||
summary: Get one attachment.
|
summary: Get one attachment.
|
||||||
tags:
|
tags:
|
||||||
- task
|
- task
|
||||||
|
/tasks/{id}/position:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Updates a task position.
|
||||||
|
parameters:
|
||||||
|
- description: Task ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The task position with updated values you want to change.
|
||||||
|
in: body
|
||||||
|
name: view
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.TaskPosition'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The updated task position.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.TaskPosition'
|
||||||
|
"400":
|
||||||
|
description: Invalid task position object provided.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/web.HTTPError'
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
security:
|
||||||
|
- JWTKeyAuth: []
|
||||||
|
summary: Updates a task position
|
||||||
|
tags:
|
||||||
|
- task
|
||||||
/tasks/{task}/labels:
|
/tasks/{task}/labels:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user