diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md
index 4aff46443..8183f44c3 100644
--- a/docs/content/doc/usage/errors.md
+++ b/docs/content/doc/usage/errors.md
@@ -55,19 +55,20 @@ This document describes the different errors Vikunja can return.
## Project
-| ErrorCode | HTTP Status Code | Description |
-|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------------|
-| 3001 | 404 | The project does not exist. |
-| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
-| 3005 | 400 | The project title cannot be empty. |
-| 3006 | 404 | The project share does not exist. |
-| 3007 | 400 | A project with this identifier already exists. |
+| ErrorCode | HTTP Status Code | Description |
+|-----------|------------------|------------------------------------------------------------------------------------------------------------------------------------|
+| 3001 | 404 | The project does not exist. |
+| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
+| 3005 | 400 | The project title cannot be empty. |
+| 3006 | 404 | The project share does not exist. |
+| 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. |
-| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". |
-| 3010 | 412 | This project cannot be a child of itself. |
-| 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. |
-| 3013 | 412 | This project cannot be archived because a user has set it as their default project. |
+| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". |
+| 3010 | 412 | This project cannot be a child of itself. |
+| 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. |
+| 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
@@ -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. |
| 4024 | 400 | The provided filter expression is invalid. |
| 4025 | 400 | The reaction kind is invalid. |
+| 4026 | 400 | You must provide a project view ID when sorting by position. |
## Team
diff --git a/frontend/cypress/e2e/project/prepareProjects.ts b/frontend/cypress/e2e/project/prepareProjects.ts
index ea7a1b01d..27c8b20df 100644
--- a/frontend/cypress/e2e/project/prepareProjects.ts
+++ b/frontend/cypress/e2e/project/prepareProjects.ts
@@ -1,15 +1,50 @@
import {ProjectFactory} from '../../factories/project'
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() {
const projects = ProjectFactory.create(1, {
title: 'First Project'
})
TaskFactory.truncate()
+ projects.views = createDefaultViews(projects[0].id)
return projects
}
-export function prepareProjects(setProjects = (...args: any[]) => {}) {
+export function prepareProjects(setProjects = (...args: any[]) => {
+}) {
beforeEach(() => {
const projects = createProjects()
setProjects(projects)
diff --git a/frontend/cypress/e2e/project/project-history.spec.ts b/frontend/cypress/e2e/project/project-history.spec.ts
index 0dadca8c7..b7caad07e 100644
--- a/frontend/cypress/e2e/project/project-history.spec.ts
+++ b/frontend/cypress/e2e/project/project-history.spec.ts
@@ -2,6 +2,7 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ProjectFactory} from '../../factories/project'
import {prepareProjects} from './prepareProjects'
+import {ProjectViewFactory} from '../../factories/project_view'
describe('Project History', () => {
createFakeUserAndLogin()
@@ -12,23 +13,28 @@ describe('Project History', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
const projects = ProjectFactory.create(6)
+ ProjectViewFactory.truncate()
+ projects.forEach(p => ProjectViewFactory.create(1, {
+ id: p.id,
+ project_id: p.id,
+ }, false))
cy.visit('/')
cy.wait('@loadProjectArray')
cy.get('body')
.should('not.contain', 'Last viewed')
- cy.visit(`/projects/${projects[0].id}`)
+ cy.visit(`/projects/${projects[0].id}/${projects[0].id}`)
cy.wait('@loadProject')
- cy.visit(`/projects/${projects[1].id}`)
+ cy.visit(`/projects/${projects[1].id}/${projects[1].id}`)
cy.wait('@loadProject')
- cy.visit(`/projects/${projects[2].id}`)
+ cy.visit(`/projects/${projects[2].id}/${projects[2].id}`)
cy.wait('@loadProject')
- cy.visit(`/projects/${projects[3].id}`)
+ cy.visit(`/projects/${projects[3].id}/${projects[3].id}`)
cy.wait('@loadProject')
- cy.visit(`/projects/${projects[4].id}`)
+ cy.visit(`/projects/${projects[4].id}/${projects[4].id}`)
cy.wait('@loadProject')
- cy.visit(`/projects/${projects[5].id}`)
+ cy.visit(`/projects/${projects[5].id}/${projects[5].id}`)
cy.wait('@loadProject')
// cy.visit('/')
diff --git a/frontend/cypress/e2e/project/project-view-gantt.spec.ts b/frontend/cypress/e2e/project/project-view-gantt.spec.ts
index 5a67c7081..507588d29 100644
--- a/frontend/cypress/e2e/project/project-view-gantt.spec.ts
+++ b/frontend/cypress/e2e/project/project-view-gantt.spec.ts
@@ -11,7 +11,7 @@ describe('Project View Gantt', () => {
it('Hides tasks with no dates', () => {
const tasks = TaskFactory.create(1)
- cy.visit('/projects/1/gantt')
+ cy.visit('/projects/1/2')
cy.get('.g-gantt-rows-container')
.should('not.contain', tasks[0].title)
@@ -25,7 +25,7 @@ describe('Project View Gantt', () => {
nextMonth.setDate(1)
nextMonth.setMonth(9)
- cy.visit('/projects/1/gantt')
+ cy.visit('/projects/1/2')
cy.get('.g-timeunits-container')
.should('contain', format(now, 'MMMM'))
@@ -38,7 +38,7 @@ describe('Project View Gantt', () => {
start_date: now.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')
.should('not.be.empty')
@@ -50,7 +50,7 @@ describe('Project View Gantt', () => {
start_date: null,
end_date: null,
})
- cy.visit('/projects/1/gantt')
+ cy.visit('/projects/1/2')
cy.get('.gantt-options .fancycheckbox')
.contains('Show tasks which don\'t have dates set')
@@ -69,7 +69,7 @@ describe('Project View Gantt', () => {
start_date: now.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')
.first()
@@ -83,7 +83,7 @@ describe('Project View Gantt', () => {
const now = Date.UTC(2022, 10, 9)
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')
.click()
@@ -99,7 +99,7 @@ describe('Project View Gantt', () => {
})
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')
.should('contain', 'September 2022')
@@ -115,7 +115,7 @@ describe('Project View Gantt', () => {
start_date: formatISO(now),
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')
.dblclick()
diff --git a/frontend/cypress/e2e/project/project-view-kanban.spec.ts b/frontend/cypress/e2e/project/project-view-kanban.spec.ts
index 2c74ba9a9..1d93e2f86 100644
--- a/frontend/cypress/e2e/project/project-view-kanban.spec.ts
+++ b/frontend/cypress/e2e/project/project-view-kanban.spec.ts
@@ -4,35 +4,65 @@ import {BucketFactory} from '../../factories/bucket'
import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
+import {ProjectViewFactory} from "../../factories/project_view";
+import {TaskBucketFactory} from "../../factories/task_buckets";
function createSingleTaskInBucket(count = 1, attrs = {}) {
const projects = ProjectFactory.create(1)
- const buckets = BucketFactory.create(2, {
+ const views = ProjectViewFactory.create(1, {
+ id: 1,
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, {
project_id: projects[0].id,
bucket_id: buckets[0].id,
...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', () => {
createFakeUserAndLogin()
prepareProjects()
-
+
let buckets
beforeEach(() => {
- buckets = BucketFactory.create(2)
+ buckets = BucketFactory.create(2, {
+ project_view_id: 4,
+ })
})
it('Shows all buckets with their tasks', () => {
- const data = TaskFactory.create(10, {
- project_id: 1,
- bucket_id: 1,
- })
- cy.visit('/projects/1/kanban')
+ const data = createTaskWithBuckets(buckets, 10)
+ cy.visit('/projects/1/4')
cy.get('.kanban .bucket .title')
.contains(buckets[0].title)
@@ -46,11 +76,8 @@ describe('Project View Kanban', () => {
})
it('Can add a new task to a bucket', () => {
- TaskFactory.create(2, {
- project_id: 1,
- bucket_id: 1,
- })
- cy.visit('/projects/1/kanban')
+ createTaskWithBuckets(buckets, 2)
+ cy.visit('/projects/1/4')
cy.get('.kanban .bucket')
.contains(buckets[0].title)
@@ -68,7 +95,7 @@ describe('Project View Kanban', () => {
})
it('Can create a new bucket', () => {
- cy.visit('/projects/1/kanban')
+ cy.visit('/projects/1/4')
cy.get('.kanban .bucket.new-bucket .button')
.click()
@@ -82,7 +109,7 @@ describe('Project View Kanban', () => {
})
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')
.first()
@@ -103,7 +130,7 @@ describe('Project View Kanban', () => {
})
it('Can rename a bucket', () => {
- cy.visit('/projects/1/kanban')
+ cy.visit('/projects/1/4')
cy.get('.kanban .bucket .bucket-header .title')
.first()
@@ -114,7 +141,7 @@ describe('Project View Kanban', () => {
})
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')
.first()
@@ -137,17 +164,14 @@ describe('Project View Kanban', () => {
})
it('Can drag tasks around', () => {
- const tasks = TaskFactory.create(2, {
- project_id: 1,
- bucket_id: 1,
- })
- cy.visit('/projects/1/kanban')
+ const tasks = createTaskWithBuckets(buckets, 2)
+ cy.visit('/projects/1/4')
cy.get('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
.first()
.drag('.kanban .bucket:nth-child(2) .tasks')
-
+
cy.get('.kanban .bucket:nth-child(2) .tasks')
.should('contain', tasks[0].title)
cy.get('.kanban .bucket:nth-child(1) .tasks')
@@ -155,12 +179,8 @@ describe('Project View Kanban', () => {
})
it('Should navigate to the task when the task card is clicked', () => {
- const tasks = TaskFactory.create(5, {
- id: '{increment}',
- project_id: 1,
- bucket_id: 1,
- })
- cy.visit('/projects/1/kanban')
+ const tasks = createTaskWithBuckets(buckets, 5)
+ cy.visit('/projects/1/4')
cy.get('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
@@ -168,28 +188,33 @@ describe('Project View Kanban', () => {
.click()
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', () => {
const projects = ProjectFactory.create(2)
- BucketFactory.create(2, {
+ const views = ProjectViewFactory.create(2, {
project_id: '{increment}',
+ view_kind: 3,
+ bucket_configuration_mode: 1,
})
+ BucketFactory.create(2)
const tasks = TaskFactory.create(5, {
id: '{increment}',
project_id: 1,
- bucket_id: 1,
+ })
+ TaskBucketFactory.create(5, {
+ project_view_id: 1,
})
const task = tasks[0]
- cy.visit('/projects/1/kanban')
+ cy.visit('/projects/1/'+views[0].id)
cy.get('.kanban .bucket .tasks .task')
.contains(task.title)
.should('be.visible')
.click()
- cy.get('.task-view .action-buttons .button', { timeout: 3000 })
+ cy.get('.task-view .action-buttons .button', {timeout: 3000})
.contains('Move')
.click()
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
@@ -201,27 +226,23 @@ describe('Project View Kanban', () => {
.first()
.click()
- cy.get('.global-notification', { timeout: 1000 })
+ cy.get('.global-notification', {timeout: 1000})
.should('contain', 'Success')
cy.go('back')
cy.get('.kanban .bucket')
.should('not.contain', task.title)
})
-
+
it('Shows a button to filter the kanban board', () => {
- const data = TaskFactory.create(10, {
- project_id: 1,
- bucket_id: 1,
- })
- cy.visit('/projects/1/kanban')
-
+ cy.visit('/projects/1/4')
+
cy.get('.project-kanban .filter-container .base-button')
.should('exist')
})
-
+
it('Should remove a task from the board when deleting it', () => {
- const task = createSingleTaskInBucket(5)
- cy.visit('/projects/1/kanban')
+ const {task, view} = createSingleTaskInBucket(5)
+ cy.visit(`/projects/1/${view.id}`)
cy.get('.kanban .bucket .tasks .task')
.contains(task.title)
@@ -239,18 +260,18 @@ describe('Project View Kanban', () => {
cy.get('.global-notification')
.should('contain', 'Success')
-
+
cy.get('.kanban .bucket .tasks')
.should('not.contain', task.title)
})
it('Should show a task description icon if the task has a description', () => {
- cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
- const task = createSingleTaskInBucket(1, {
+ cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
+ const {task, view} = createSingleTaskInBucket(1, {
description: 'Lorem Ipsum',
})
- cy.visit(`/projects/${task.project_id}/kanban`)
+ cy.visit(`/projects/${task.project_id}/${view.id}`)
cy.wait('@loadTasks')
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', () => {
- cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
- const task = createSingleTaskInBucket(1, {
+ cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
+ const {task, view} = createSingleTaskInBucket(1, {
description: '',
})
- cy.visit(`/projects/${task.project_id}/kanban`)
+ cy.visit(`/projects/${task.project_id}/${view.id}`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
@@ -271,15 +292,15 @@ describe('Project View Kanban', () => {
})
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')
- const task = createSingleTaskInBucket(1, {
+ cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
+ const {task, view} = createSingleTaskInBucket(1, {
description: '
',
})
- cy.visit(`/projects/${task.project_id}/kanban`)
+ cy.visit(`/projects/${task.project_id}/${view.id}`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('not.exist')
})
-})
\ No newline at end of file
+})
diff --git a/frontend/cypress/e2e/project/project-view-list.spec.ts b/frontend/cypress/e2e/project/project-view-list.spec.ts
index c325f6824..1cb6f0c94 100644
--- a/frontend/cypress/e2e/project/project-view-list.spec.ts
+++ b/frontend/cypress/e2e/project/project-view-list.spec.ts
@@ -5,15 +5,16 @@ import {TaskFactory} from '../../factories/task'
import {UserFactory} from '../../factories/user'
import {ProjectFactory} from '../../factories/project'
import {prepareProjects} from './prepareProjects'
+import {BucketFactory} from '../../factories/bucket'
-describe('Project View Project', () => {
+describe('Project View List', () => {
createFakeUserAndLogin()
prepareProjects()
it('Should be an empty project', () => {
cy.visit('/projects/1')
cy.url()
- .should('contain', '/projects/1/list')
+ .should('contain', '/projects/1/1')
cy.get('.project-title')
.should('contain', 'First Project')
cy.get('.project-title-dropdown')
@@ -24,6 +25,10 @@ describe('Project View Project', () => {
})
it('Should create a new task', () => {
+ BucketFactory.create(2, {
+ project_view_id: 4,
+ })
+
const newTaskTitle = 'New task'
cy.visit('/projects/1')
@@ -38,7 +43,7 @@ describe('Project View Project', () => {
id: '{increment}',
project_id: 1,
})
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.get('.tasks .task .tasktext')
.contains(tasks[0].title)
@@ -88,10 +93,10 @@ describe('Project View Project', () => {
title: i => `task${i}`,
project_id: 1,
})
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.get('.tasks')
- .should('contain', tasks[1].title)
+ .should('contain', tasks[20].title)
cy.get('.tasks')
.should('not.contain', tasks[99].title)
@@ -104,6 +109,6 @@ describe('Project View Project', () => {
cy.get('.tasks')
.should('contain', tasks[99].title)
cy.get('.tasks')
- .should('not.contain', tasks[1].title)
+ .should('not.contain', tasks[20].title)
})
})
\ No newline at end of file
diff --git a/frontend/cypress/e2e/project/project-view-table.spec.ts b/frontend/cypress/e2e/project/project-view-table.spec.ts
index d468e61c2..6319617c1 100644
--- a/frontend/cypress/e2e/project/project-view-table.spec.ts
+++ b/frontend/cypress/e2e/project/project-view-table.spec.ts
@@ -1,13 +1,15 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
+import {prepareProjects} from './prepareProjects'
describe('Project View Table', () => {
createFakeUserAndLogin()
+ prepareProjects()
it('Should show a table with tasks', () => {
const tasks = TaskFactory.create(1)
- cy.visit('/projects/1/table')
+ cy.visit('/projects/1/3')
cy.get('.project-table table.table')
.should('exist')
@@ -17,7 +19,7 @@ describe('Project View Table', () => {
it('Should have working column switches', () => {
TaskFactory.create(1)
- cy.visit('/projects/1/table')
+ cy.visit('/projects/1/3')
cy.get('.project-table .filter-container .items .button')
.contains('Columns')
@@ -42,7 +44,7 @@ describe('Project View Table', () => {
id: '{increment}',
project_id: 1,
})
- cy.visit('/projects/1/table')
+ cy.visit('/projects/1/3')
cy.get('.project-table table.table')
.contains(tasks[0].title)
diff --git a/frontend/cypress/e2e/project/project.spec.ts b/frontend/cypress/e2e/project/project.spec.ts
index a5ef6cabc..7258b59fd 100644
--- a/frontend/cypress/e2e/project/project.spec.ts
+++ b/frontend/cypress/e2e/project/project.spec.ts
@@ -33,14 +33,14 @@ describe('Projects', () => {
})
it('Should redirect to a specific project view after visited', () => {
- cy.intercept(Cypress.env('API_URL') + '/projects/*/buckets*').as('loadBuckets')
- cy.visit('/projects/1/kanban')
+ cy.intercept(Cypress.env('API_URL') + '/projects/*/views/*/tasks**').as('loadBuckets')
+ cy.visit('/projects/1/4')
cy.url()
- .should('contain', '/projects/1/kanban')
+ .should('contain', '/projects/1/4')
cy.wait('@loadBuckets')
cy.visit('/projects/1')
cy.url()
- .should('contain', '/projects/1/kanban')
+ .should('contain', '/projects/1/4')
})
it('Should rename the project in all places', () => {
diff --git a/frontend/cypress/e2e/sharing/linkShare.spec.ts b/frontend/cypress/e2e/sharing/linkShare.spec.ts
index 99b6fb5a0..ffb6ea3a7 100644
--- a/frontend/cypress/e2e/sharing/linkShare.spec.ts
+++ b/frontend/cypress/e2e/sharing/linkShare.spec.ts
@@ -1,9 +1,9 @@
import {LinkShareFactory} from '../../factories/link_sharing'
-import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
+import {createProjects} from '../project/prepareProjects'
function prepareLinkShare() {
- const projects = ProjectFactory.create(1)
+ const projects = createProjects()
const tasks = TaskFactory.create(10, {
project_id: projects[0].id
})
@@ -32,13 +32,13 @@ describe('Link shares', () => {
cy.get('.tasks')
.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', () => {
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')
.should('contain', project.title)
diff --git a/frontend/cypress/e2e/task/overview.spec.ts b/frontend/cypress/e2e/task/overview.spec.ts
index 342134b23..ab10dc1b0 100644
--- a/frontend/cypress/e2e/task/overview.spec.ts
+++ b/frontend/cypress/e2e/task/overview.spec.ts
@@ -5,11 +5,13 @@ import {seed} from '../../support/seed'
import {TaskFactory} from '../../factories/task'
import {BucketFactory} from '../../factories/bucket'
import {updateUserSettings} from '../../support/updateUserSettings'
+import {createDefaultViews} from "../project/prepareProjects";
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
const project = ProjectFactory.create()[0]
+ const views = createDefaultViews(project.id)
BucketFactory.create(1, {
- project_id: project.id,
+ project_view_id: views[3].id,
})
const tasks = []
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', () => {
- const {tasks} = seedTasks()
+ const {tasks} = seedTasks(49)
const newTaskTitle = 'New Task'
cy.visit('/')
@@ -71,9 +73,8 @@ describe('Home Page Task Overview', () => {
due_date: new Date().toISOString(),
}, false)
- cy.visit(`/projects/${tasks[0].project_id}/list`)
+ cy.visit(`/projects/${tasks[0].project_id}/1`)
cy.get('.tasks .task')
- .first()
.should('contain.text', newTaskTitle)
cy.visit('/')
cy.get('[data-cy="showTasks"] .card .task')
@@ -88,7 +89,7 @@ describe('Home Page Task Overview', () => {
cy.visit('/')
- cy.visit(`/projects/${tasks[0].project_id}/list`)
+ cy.visit(`/projects/${tasks[0].project_id}/1`)
cy.get('.task-add textarea')
.type(newTaskTitle+'{enter}')
cy.visit('/')
diff --git a/frontend/cypress/e2e/task/task.spec.ts b/frontend/cypress/e2e/task/task.spec.ts
index aa1653e8b..768f3d61c 100644
--- a/frontend/cypress/e2e/task/task.spec.ts
+++ b/frontend/cypress/e2e/task/task.spec.ts
@@ -12,6 +12,7 @@ import {BucketFactory} from '../../factories/bucket'
import {TaskAttachmentFactory} from '../../factories/task_attachments'
import {TaskReminderFactory} from '../../factories/task_reminders'
+import {createDefaultViews} from "../project/prepareProjects";
function addLabelToTaskAndVerify(labelTitle: string) {
cy.get('.task-view .action-buttons .button')
@@ -53,15 +54,16 @@ describe('Task', () => {
beforeEach(() => {
// UserFactory.create(1)
projects = ProjectFactory.create(1)
+ const views = createDefaultViews(projects[0].id)
buckets = BucketFactory.create(1, {
- project_id: projects[0].id,
+ project_view_id: views[3].id,
})
TaskFactory.truncate()
UserProjectFactory.truncate()
})
it('Should be created new', () => {
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.get('.input[placeholder="Add a new task…"')
.type('New Task')
cy.get('.button')
@@ -75,7 +77,7 @@ describe('Task', () => {
it('Inserts new tasks at the top of the project', () => {
TaskFactory.create(1)
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.get('.project-is-empty-notice')
.should('not.exist')
cy.get('.input[placeholder="Add a new task…"')
@@ -93,7 +95,7 @@ describe('Task', () => {
it('Marks a task as done', () => {
TaskFactory.create(1)
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.get('.tasks .task .fancycheckbox')
.first()
.click()
@@ -104,7 +106,7 @@ describe('Task', () => {
it('Can add a task to favorites', () => {
TaskFactory.create(1)
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.get('.tasks .task .favorite')
.first()
.click()
@@ -113,12 +115,12 @@ describe('Task', () => {
})
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, {
description: 'Lorem Ipsum',
})
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.wait('@loadTasks')
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', () => {
- 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, {
description: '',
})
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.wait('@loadTasks')
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', () => {
- 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, {
description: '',
})
- cy.visit('/projects/1/list')
+ cy.visit('/projects/1/1')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
@@ -314,8 +316,9 @@ describe('Task', () => {
it('Can move a task to another project', () => {
const projects = ProjectFactory.create(2)
+ const views = createDefaultViews(projects[0].id)
BucketFactory.create(2, {
- project_id: '{increment}',
+ project_view_id: views[3].id,
})
const tasks = TaskFactory.create(1, {
id: 1,
@@ -469,7 +472,7 @@ describe('Task', () => {
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
- cy.visit(`/projects/${projects[0].id}/kanban`)
+ cy.visit(`/projects/${projects[0].id}/4`)
cy.get('.bucket .task')
.contains(tasks[0].title)
@@ -836,7 +839,7 @@ describe('Task', () => {
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
- cy.visit(`/projects/${projects[0].id}/kanban`)
+ cy.visit(`/projects/${projects[0].id}/4`)
cy.get('.bucket .task')
.contains(tasks[0].title)
diff --git a/frontend/cypress/factories/bucket.ts b/frontend/cypress/factories/bucket.ts
index 2e0e91077..23b1cfe60 100644
--- a/frontend/cypress/factories/bucket.ts
+++ b/frontend/cypress/factories/bucket.ts
@@ -10,7 +10,7 @@ export class BucketFactory extends Factory {
return {
id: '{increment}',
title: faker.lorem.words(3),
- project_id: 1,
+ project_view_id: '{increment}',
created_by_id: 1,
created: now.toISOString(),
updated: now.toISOString(),
diff --git a/frontend/cypress/factories/project_view.ts b/frontend/cypress/factories/project_view.ts
new file mode 100644
index 000000000..710afff9c
--- /dev/null
+++ b/frontend/cypress/factories/project_view.ts
@@ -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(),
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/cypress/factories/task.ts b/frontend/cypress/factories/task.ts
index bc97446a8..9c37ad0f7 100644
--- a/frontend/cypress/factories/task.ts
+++ b/frontend/cypress/factories/task.ts
@@ -14,7 +14,6 @@ export class TaskFactory extends Factory {
project_id: 1,
created_by_id: 1,
index: '{increment}',
- position: '{increment}',
created: now.toISOString(),
updated: now.toISOString()
}
diff --git a/frontend/cypress/factories/task_buckets.ts b/frontend/cypress/factories/task_buckets.ts
new file mode 100644
index 000000000..a91141d9d
--- /dev/null
+++ b/frontend/cypress/factories/task_buckets.ts
@@ -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}',
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/home/contentAuth.vue b/frontend/src/components/home/contentAuth.vue
index 51248f140..c2c814b4b 100644
--- a/frontend/src/components/home/contentAuth.vue
+++ b/frontend/src/components/home/contentAuth.vue
@@ -37,7 +37,7 @@
v-slot="{ Component }"
:route="routeWithModal"
>
-
+
diff --git a/frontend/src/components/home/contentLinkShare.vue b/frontend/src/components/home/contentLinkShare.vue
index b21ee1ea5..95cf3dc15 100644
--- a/frontend/src/components/home/contentLinkShare.vue
+++ b/frontend/src/components/home/contentLinkShare.vue
@@ -33,11 +33,15 @@ import {useBaseStore} from '@/stores/base'
import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue'
+import {useProjectStore} from '@/stores/projects'
const baseStore = useBaseStore()
const currentProject = computed(() => baseStore.currentProject)
const background = computed(() => baseStore.background)
const logoVisible = computed(() => baseStore.logoVisible)
+
+const projectStore = useProjectStore()
+projectStore.loadAllProjects()
\ No newline at end of file
diff --git a/frontend/src/components/sharing/linkSharing.vue b/frontend/src/components/sharing/linkSharing.vue
index 88aaa61ad..b63d0805d 100644
--- a/frontend/src/components/sharing/linkSharing.vue
+++ b/frontend/src/components/sharing/linkSharing.vue
@@ -173,11 +173,11 @@
@@ -230,9 +230,9 @@ import LinkShareService from '@/services/linkShare'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message'
import {getDisplayName} from '@/models/user'
-import type {ProjectView} from '@/types/ProjectView'
-import {PROJECT_VIEWS} from '@/types/ProjectView'
import {useConfigStore} from '@/stores/config'
+import {useProjectStore} from '@/stores/projects'
+import type {IProjectView} from '@/modelTypes/IProjectView'
const props = defineProps({
projectId: {
@@ -252,17 +252,13 @@ const showDeleteModal = ref(false)
const linkIdToDelete = ref(0)
const showNewForm = ref(false)
-type SelectedViewMapper = Record
+type SelectedViewMapper = Record
const selectedView = ref({})
-const availableViews = computed>(() => ({
- list: t('project.list.title'),
- gantt: t('project.gantt.title'),
- table: t('project.table.title'),
- kanban: t('project.kanban.title'),
-}))
+const projectStore = useProjectStore()
+const availableViews = computed(() => projectStore.projects[props.projectId]?.views || [])
const copy = useCopyToClipboard()
watch(
() => props.projectId,
@@ -281,7 +277,7 @@ async function load(projectId: IProject['id']) {
const links = await linkShareService.getAll({projectId})
links.forEach((l: ILinkShare) => {
- selectedView.value[l.id] = 'list'
+ selectedView.value[l.id] = availableViews.value[0].id
})
linkShares.value = links
}
@@ -315,8 +311,8 @@ async function remove(projectId: IProject['id']) {
}
}
-function getShareLink(hash: string, view: ProjectView = PROJECT_VIEWS.LIST) {
- return frontendUrl.value + 'share/' + hash + '/auth?view=' + view
+function getShareLink(hash: string, viewId: IProjectView['id']) {
+ return frontendUrl.value + 'share/' + hash + '/auth?view=' + viewId
}
diff --git a/frontend/src/components/tasks/partials/singleTaskInProject.vue b/frontend/src/components/tasks/partials/singleTaskInProject.vue
index 95d9f8888..2951c39cd 100644
--- a/frontend/src/components/tasks/partials/singleTaskInProject.vue
+++ b/frontend/src/components/tasks/partials/singleTaskInProject.vue
@@ -30,7 +30,7 @@
@@ -136,7 +136,7 @@
{{ project.title }}
diff --git a/frontend/src/composables/useRouteWithModal.ts b/frontend/src/composables/useRouteWithModal.ts
index 6cf9a66dc..4857b3c67 100644
--- a/frontend/src/composables/useRouteWithModal.ts
+++ b/frontend/src/composables/useRouteWithModal.ts
@@ -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 {useBaseStore} from '@/stores/base'
+import {useProjectStore} from '@/stores/projects'
export function useRouteWithModal() {
const router = useRouter()
const route = useRoute()
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
const baseStore = useBaseStore()
+ const projectStore = useProjectStore()
const routeWithModal = computed(() => {
return backdropView.value
@@ -29,7 +31,7 @@ export function useRouteWithModal() {
if (routePropsOption === true) {
routeProps = route.params
} else {
- if(typeof routePropsOption === 'function') {
+ if (typeof routePropsOption === 'function') {
routeProps = routePropsOption(route)
} else {
routeProps = routePropsOption
@@ -52,7 +54,7 @@ export function useRouteWithModal() {
}
currentModal.value = h(component, routeProps)
})
-
+
const historyState = computed(() => route.fullPath && window.history.state)
function closeModal() {
@@ -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,
// 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
- const kanbanRouteMatch = new RegExp('\\/projects\\/\\d+\\/kanban', 'g')
- const kanbanRouter = {name: 'project.kanban', params: {projectId: baseStore.currentProject?.id}}
- if (kanbanRouteMatch.test(historyState.value.back)
- && baseStore.currentProject
- && historyState.value.back !== router.resolve(kanbanRouter).fullPath) {
- router.push(kanbanRouter)
+ const routeMatch = new RegExp('\\/projects\\/\\d+\\/(\\d+)', 'g')
+ const match = routeMatch.exec(historyState.value.back)
+ if (match !== null && baseStore.currentProject) {
+ let viewId: string | number = match[1]
+
+ 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
}
diff --git a/frontend/src/composables/useTaskList.ts b/frontend/src/composables/useTaskList.ts
index ef048dc7a..27a8ab6eb 100644
--- a/frontend/src/composables/useTaskList.ts
+++ b/frontend/src/composables/useTaskList.ts
@@ -7,6 +7,7 @@ import type {ITask} from '@/modelTypes/ITask'
import {error} from '@/message'
import type {IProject} from '@/modelTypes/IProject'
import {useAuthStore} from '@/stores/auth'
+import type {IProjectView} from '@/modelTypes/IProjectView'
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.
*/
-export function useTaskList(projectIdGetter: ComputedGetter, sortByDefault: SortBy = SORT_BY_DEFAULT) {
+export function useTaskList(
+ projectIdGetter: ComputedGetter,
+ projectViewIdGetter: ComputedGetter,
+ sortByDefault: SortBy = SORT_BY_DEFAULT,
+) {
const projectId = computed(() => projectIdGetter())
+ const projectViewId = computed(() => projectViewIdGetter())
const params = ref({...getDefaultTaskFilterParams()})
@@ -87,7 +93,10 @@ export function useTaskList(projectIdGetter: ComputedGetter, sor
const getAllTasksParams = computed(() => {
return [
- {projectId: projectId.value},
+ {
+ projectId: projectId.value,
+ viewId: projectViewId.value,
+ },
{
...allParams.value,
filter_timezone: authStore.settings.timezone,
diff --git a/frontend/src/helpers/projectView.ts b/frontend/src/helpers/projectView.ts
index 9bc3a7747..2c8cfb763 100644
--- a/frontend/src/helpers/projectView.ts
+++ b/frontend/src/helpers/projectView.ts
@@ -1,64 +1,17 @@
-import type { RouteRecordName } from 'vue-router'
-import router from '@/router'
-
import type {IProject} from '@/modelTypes/IProject'
-export type ProjectRouteName = Extract
-export type ProjectViewSettings = Record<
- IProject['id'],
- Extract
->
+export type ProjectViewSettings = Record
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
*/
-export function saveProjectView(projectId: IProject['id'], routeName: string) {
- if (routeName.includes('settings.')) {
+export function saveProjectView(projectId: IProject['id'], viewId: number) {
+ if (!projectId || !viewId) {
return
}
-
- if (!projectId) {
- return
- }
-
+
// We use local storage and not the store here to make it persistent across reloads.
const savedProjectView = localStorage.getItem(SETTINGS_KEY_PROJECT_VIEW)
let savedProjectViewSettings: ProjectViewSettings | false = false
@@ -71,30 +24,19 @@ export function saveProjectView(projectId: IProject['id'], routeName: string) {
projectViewSettings = savedProjectViewSettings
}
- projectViewSettings[projectId] = routeName
+ projectViewSettings[projectId] = viewId
localStorage.setItem(SETTINGS_KEY_PROJECT_VIEW, JSON.stringify(projectViewSettings))
}
-export const getProjectView = (projectId: IProject['id']) => {
- // TODO: remove migration when releasing 1.0
- const migratedProjectView = migrateStoredProjectRouteSettings()
-
- if (migratedProjectView !== undefined && router.hasRoute(migratedProjectView)) {
- return migratedProjectView
+export function getProjectViewId(projectId: IProject['id']): number {
+ const projectViewSettingsString = localStorage.getItem(SETTINGS_KEY_PROJECT_VIEW)
+ if (!projectViewSettingsString) {
+ return 0
}
- try {
- const projectViewSettingsString = localStorage.getItem(SETTINGS_KEY_PROJECT_VIEW)
- if (!projectViewSettingsString) {
- throw new Error()
- }
-
- const projectViewSettings = JSON.parse(projectViewSettingsString) as ProjectViewSettings
- if (!router.hasRoute(projectViewSettings[projectId])) {
- throw new Error()
- }
- return projectViewSettings[projectId]
- } catch (e) {
- return
- }
+ const projectViewSettings = JSON.parse(projectViewSettingsString) as ProjectViewSettings
+ if (isNaN(projectViewSettings[projectId])) {
+ return 0
+ }
+ return projectViewSettings[projectId]
}
\ No newline at end of file
diff --git a/frontend/src/i18n/lang/en.json b/frontend/src/i18n/lang/en.json
index b9dcefc4c..9d39969c0 100644
--- a/frontend/src/i18n/lang/en.json
+++ b/frontend/src/i18n/lang/en.json
@@ -381,6 +381,22 @@
"secret": "Secret",
"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."
+ },
+ "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": {
@@ -1049,7 +1065,8 @@
"newProject": "New project",
"createProject": "Create 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": {
"url": "Vikunja URL",
diff --git a/frontend/src/modelTypes/IBucket.ts b/frontend/src/modelTypes/IBucket.ts
index 0f7ce12da..dccd0d842 100644
--- a/frontend/src/modelTypes/IBucket.ts
+++ b/frontend/src/modelTypes/IBucket.ts
@@ -1,6 +1,7 @@
import type {IAbstract} from './IAbstract'
import type {IUser} from './IUser'
import type {ITask} from './ITask'
+import type {IProjectView} from '@/modelTypes/IProjectView'
export interface IBucket extends IAbstract {
id: number
@@ -10,6 +11,7 @@ export interface IBucket extends IAbstract {
tasks: ITask[]
position: number
count: number
+ projectViewId: IProjectView['id']
createdBy: IUser
created: Date
diff --git a/frontend/src/modelTypes/IProject.ts b/frontend/src/modelTypes/IProject.ts
index f5c976f3a..c4e212e83 100644
--- a/frontend/src/modelTypes/IProject.ts
+++ b/frontend/src/modelTypes/IProject.ts
@@ -2,6 +2,7 @@ import type {IAbstract} from './IAbstract'
import type {ITask} from './ITask'
import type {IUser} from './IUser'
import type {ISubscription} from './ISubscription'
+import type {IProjectView} from '@/modelTypes/IProjectView'
export interface IProject extends IAbstract {
@@ -21,6 +22,7 @@ export interface IProject extends IAbstract {
parentProjectId: number
doneBucketId: number
defaultBucketId: number
+ views: IProjectView[]
created: Date
updated: Date
diff --git a/frontend/src/modelTypes/IProjectView.ts b/frontend/src/modelTypes/IProjectView.ts
new file mode 100644
index 000000000..6a003b8ff
--- /dev/null
+++ b/frontend/src/modelTypes/IProjectView.ts
@@ -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
+}
\ No newline at end of file
diff --git a/frontend/src/modelTypes/ITaskPosition.ts b/frontend/src/modelTypes/ITaskPosition.ts
new file mode 100644
index 000000000..7ea71e1c0
--- /dev/null
+++ b/frontend/src/modelTypes/ITaskPosition.ts
@@ -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
+}
\ No newline at end of file
diff --git a/frontend/src/models/project.ts b/frontend/src/models/project.ts
index 145262dc3..06c9e8ee7 100644
--- a/frontend/src/models/project.ts
+++ b/frontend/src/models/project.ts
@@ -7,6 +7,7 @@ import type {IProject} from '@/modelTypes/IProject'
import type {IUser} from '@/modelTypes/IUser'
import type {ITask} from '@/modelTypes/ITask'
import type {ISubscription} from '@/modelTypes/ISubscription'
+import ProjectViewModel from '@/models/projectView'
export default class ProjectModel extends AbstractModel implements IProject {
id = 0
@@ -25,6 +26,7 @@ export default class ProjectModel extends AbstractModel implements IPr
parentProjectId = 0
doneBucketId = 0
defaultBucketId = 0
+ views = []
created: Date = null
updated: Date = null
@@ -48,6 +50,8 @@ export default class ProjectModel extends AbstractModel implements IPr
this.subscription = new SubscriptionModel(this.subscription)
}
+ this.views = this.views.map(v => new ProjectViewModel(v))
+
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}
diff --git a/frontend/src/models/projectView.ts b/frontend/src/models/projectView.ts
new file mode 100644
index 000000000..559f94818
--- /dev/null
+++ b/frontend/src/models/projectView.ts
@@ -0,0 +1,29 @@
+import type {IProjectView, ProjectViewBucketConfigurationMode, ProjectViewKind} from '@/modelTypes/IProjectView'
+import AbstractModel from '@/models/abstractModel'
+
+export default class ProjectViewModel extends AbstractModel 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) {
+ super()
+ this.assignData(data)
+
+ if (!this.bucketConfiguration) {
+ this.bucketConfiguration = []
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/models/taskPosition.ts b/frontend/src/models/taskPosition.ts
new file mode 100644
index 000000000..e1c37ae0b
--- /dev/null
+++ b/frontend/src/models/taskPosition.ts
@@ -0,0 +1,13 @@
+import AbstractModel from '@/models/abstractModel'
+import type {ITaskPosition} from '@/modelTypes/ITaskPosition'
+
+export default class TaskPositionModel extends AbstractModel implements ITaskPosition {
+ position = 0
+ projectViewId = 0
+ taskId = 0
+
+ constructor(data: Partial) {
+ super()
+ this.assignData(data)
+ }
+}
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 775a6f562..a5f94d45c 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -2,13 +2,11 @@ import { createRouter, createWebHistory } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import {saveLastVisited} from '@/helpers/saveLastVisited'
-import {saveProjectView, getProjectView} from '@/helpers/projectView'
+import {saveProjectView, getProjectViewId} from '@/helpers/projectView'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
-import {setTitle} from '@/helpers/setTitle'
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
-import {useProjectStore} from '@/stores/projects'
import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
@@ -33,15 +31,8 @@ const NewLabelComponent = () => import('@/views/labels/NewLabel.vue')
// Migration
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
-// Project Views
-const ProjectList = () => import('@/views/project/ProjectList.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'
+// Project View
+import ProjectView from '@/views/project/ProjectView.vue'
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
// Project Settings
@@ -53,6 +44,7 @@ const ProjectSettingShare = () => import('@/views/project/settings/share.vue')
const ProjectSettingWebhooks = () => import('@/views/project/settings/webhooks.vue')
const ProjectSettingDelete = () => import('@/views/project/settings/delete.vue')
const ProjectSettingArchive = () => import('@/views/project/settings/archive.vue')
+const ProjectSettingViews = () => import('@/views/project/settings/views.vue')
// Saved Filters
const FilterNew = () => import('@/views/filters/FilterNew.vue')
@@ -315,6 +307,15 @@ const router = createRouter({
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',
name: 'filter.settings.edit',
@@ -346,55 +347,31 @@ const router = createRouter({
path: '/projects/:projectId',
name: 'project.index',
redirect(to) {
- // Redirect the user to list view by default
- const savedProjectView = getProjectView(Number(to.params.projectId as string))
+ const viewId = getProjectViewId(Number(to.params.projectId as string))
+ console.log(viewId)
- if (savedProjectView) {
- console.log('Replaced list view with', savedProjectView)
+ if (viewId) {
+ console.debug('Replaced list view with', viewId)
}
return {
- name: savedProjectView || 'project.list',
- params: {projectId: to.params.projectId},
+ name: 'project.view',
+ params: {
+ projectId: parseInt(to.params.projectId as string),
+ viewId: viewId ?? 0,
+ },
}
},
},
{
- path: '/projects/:projectId/list',
- name: 'project.list',
- component: ProjectList,
- beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
- props: route => ({ projectId: Number(route.params.projectId as string) }),
- },
- {
- 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: '/projects/:projectId/:viewId',
+ name: 'project.view',
+ component: ProjectView,
+ beforeEnter: (to) => saveProjectView(parseInt(to.params.projectId as string), parseInt(to.params.viewId as string)),
+ props: route => ({
+ projectId: parseInt(route.params.projectId as string),
+ viewId: route.params.viewId ? parseInt(route.params.viewId as string): undefined,
+ }),
},
{
path: '/teams',
diff --git a/frontend/src/services/bucket.ts b/frontend/src/services/bucket.ts
index 0ef42f876..a353093ff 100644
--- a/frontend/src/services/bucket.ts
+++ b/frontend/src/services/bucket.ts
@@ -6,10 +6,10 @@ import type { IBucket } from '@/modelTypes/IBucket'
export default class BucketService extends AbstractService {
constructor() {
super({
- getAll: '/projects/{projectId}/buckets',
- create: '/projects/{projectId}/buckets',
- update: '/projects/{projectId}/buckets/{id}',
- delete: '/projects/{projectId}/buckets/{id}',
+ getAll: '/projects/{projectId}/views/{projectViewId}/buckets',
+ create: '/projects/{projectId}/views/{projectViewId}/buckets',
+ update: '/projects/{projectId}/views/{projectViewId}/buckets/{id}',
+ delete: '/projects/{projectId}/views/{projectViewId}/buckets/{id}',
})
}
diff --git a/frontend/src/services/projectViews.ts b/frontend/src/services/projectViews.ts
new file mode 100644
index 000000000..a35d0f325
--- /dev/null
+++ b/frontend/src/services/projectViews.ts
@@ -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 {
+ 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): ProjectViewModel {
+ return new ProjectViewModel(data)
+ }
+}
diff --git a/frontend/src/services/taskCollection.ts b/frontend/src/services/taskCollection.ts
index 046c3f0d9..3304227f1 100644
--- a/frontend/src/services/taskCollection.ts
+++ b/frontend/src/services/taskCollection.ts
@@ -2,6 +2,7 @@ import AbstractService from '@/services/abstractService'
import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
+import BucketModel from '@/models/bucket'
export interface TaskFilterParams {
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 {
constructor() {
super({
- getAll: '/projects/{projectId}/tasks',
+ getAll: '/projects/{projectId}/views/{viewId}/tasks',
})
}
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)
}
}
\ No newline at end of file
diff --git a/frontend/src/services/taskPosition.ts b/frontend/src/services/taskPosition.ts
new file mode 100644
index 000000000..c74a8038a
--- /dev/null
+++ b/frontend/src/services/taskPosition.ts
@@ -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 {
+ constructor() {
+ super({
+ update: '/tasks/{taskId}/position',
+ })
+ }
+
+ modelFactory(data: Partial) {
+ return new TaskPositionModel(data)
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/stores/kanban.ts b/frontend/src/stores/kanban.ts
index 67cbfe988..05d51e4a1 100644
--- a/frontend/src/stores/kanban.ts
+++ b/frontend/src/stores/kanban.ts
@@ -3,8 +3,6 @@ import {acceptHMRUpdate, defineStore} from 'pinia'
import {klona} from 'klona/lite'
import {findById, findIndexById} from '@/helpers/utils'
-import {i18n} from '@/i18n'
-import {success} from '@/message'
import BucketService from '@/services/bucket'
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 {IBucket} from '@/modelTypes/IBucket'
import {useAuthStore} from '@/stores/auth'
+import type {IProjectView} from '@/modelTypes/IProjectView'
const TASKS_PER_BUCKET = 25
@@ -176,10 +175,7 @@ export const useKanbanStore = defineStore('kanban', () => {
buckets.value[bucketIndex] = newBucket
}
- function addTasksToBucket({tasks, bucketId}: {
- tasks: ITask[];
- bucketId: IBucket['id'];
- }) {
+ function addTasksToBucket(tasks: ITask[], bucketId: IBucket['id']) {
const bucketIndex = findIndexById(buckets.value, bucketId)
const oldBucket = buckets.value[bucketIndex]
const newBucket = {
@@ -225,15 +221,15 @@ export const useKanbanStore = defineStore('kanban', () => {
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)
// Clear everything to prevent having old buckets in the project if loading the buckets from this project takes a few moments
setBuckets([])
- const bucketService = new BucketService()
+ const taskCollectionService = new TaskCollectionService()
try {
- const newBuckets = await bucketService.getAll({projectId}, {
+ const newBuckets = await taskCollectionService.getAll({projectId, viewId}, {
...params,
per_page: TASKS_PER_BUCKET,
})
@@ -247,6 +243,7 @@ export const useKanbanStore = defineStore('kanban', () => {
async function loadNextTasksForBucket(
projectId: IProject['id'],
+ viewId: IProjectView['id'],
ps: TaskFilterParams,
bucketId: IBucket['id'],
) {
@@ -267,7 +264,7 @@ export const useKanbanStore = defineStore('kanban', () => {
const params: TaskFilterParams = JSON.parse(JSON.stringify(ps))
- params.sort_by = ['kanban_position']
+ params.sort_by = ['position']
params.order_by = ['asc']
params.filter = `${params.filter === '' ? '' : params.filter + ' && '}bucket_id = ${bucketId}`
params.filter_timezone = authStore.settings.timezone
@@ -275,8 +272,8 @@ export const useKanbanStore = defineStore('kanban', () => {
const taskService = new TaskCollectionService()
try {
- const tasks = await taskService.getAll({projectId}, params, page)
- addTasksToBucket({tasks, bucketId: bucketId})
+ const tasks = await taskService.getAll({projectId, viewId}, params, page)
+ addTasksToBucket(tasks, bucketId)
setTasksLoadedForBucketPage({bucketId, page})
if (taskService.totalPages <= page) {
setAllTasksLoadedForBucket(bucketId)
@@ -309,7 +306,7 @@ export const useKanbanStore = defineStore('kanban', () => {
const response = await bucketService.delete(bucket)
removeBucket(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
} finally {
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 {
buckets,
isLoading: readonly(isLoading),
@@ -374,7 +359,6 @@ export const useKanbanStore = defineStore('kanban', () => {
createBucket,
deleteBucket,
updateBucket,
- updateBucketTitle,
}
})
diff --git a/frontend/src/stores/projects.ts b/frontend/src/stores/projects.ts
index 7a1992407..e6b436e6a 100644
--- a/frontend/src/stores/projects.ts
+++ b/frontend/src/stores/projects.ts
@@ -18,6 +18,7 @@ import ProjectModel from '@/models/project'
import {success} from '@/message'
import {useBaseStore} from '@/stores/base'
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
+import type {IProjectView} from '@/modelTypes/IProjectView'
const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
@@ -210,7 +211,27 @@ export const useProjectStore = defineStore('project', () => {
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 {
isLoading: readonly(isLoading),
projects: readonly(projects),
@@ -235,6 +256,8 @@ export const useProjectStore = defineStore('project', () => {
updateProject,
deleteProject,
getAncestors,
+ setProjectView,
+ removeProjectView,
}
})
diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts
index 332a4eba7..8a55c1065 100644
--- a/frontend/src/stores/tasks.ts
+++ b/frontend/src/stores/tasks.ts
@@ -28,7 +28,7 @@ import {useKanbanStore} from '@/stores/kanban'
import {useBaseStore} from '@/stores/base'
import ProjectUserService from '@/services/projectUsers'
import {useAuthStore} from '@/stores/auth'
-import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection'
+import {type TaskFilterParams} from '@/services/taskCollection'
import {getRandomColorHex} from '@/helpers/color/randomColor'
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 === '') {
params.filter_timezone = authStore.settings.timezone
}
+
+ if (projectId !== null) {
+ params.filter = 'project = '+projectId+' && (' + params.filter +')'
+ }
const cancel = setModuleLoading(setIsLoading)
try {
- if (projectId === null) {
- const taskService = new TaskService()
- tasks.value = await taskService.getAll({}, params)
- } else {
- const taskCollectionService = new TaskCollectionService()
- tasks.value = await taskCollectionService.getAll({projectId}, params)
- }
+ const taskService = new TaskService()
+ tasks.value = await taskService.getAll({}, params)
baseStore.setHasTasks(tasks.value.length > 0)
return tasks.value
} finally {
diff --git a/frontend/src/types/ProjectView.ts b/frontend/src/types/ProjectView.ts
deleted file mode 100644
index ba435ef96..000000000
--- a/frontend/src/types/ProjectView.ts
+++ /dev/null
@@ -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]
diff --git a/frontend/src/views/project/ProjectView.vue b/frontend/src/views/project/ProjectView.vue
new file mode 100644
index 000000000..055cdecd9
--- /dev/null
+++ b/frontend/src/views/project/ProjectView.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/project/helpers/useGanttFilters.ts b/frontend/src/views/project/helpers/useGanttFilters.ts
index dfe8ccd8b..e75677b75 100644
--- a/frontend/src/views/project/helpers/useGanttFilters.ts
+++ b/frontend/src/views/project/helpers/useGanttFilters.ts
@@ -12,10 +12,12 @@ import type {TaskFilterParams} from '@/services/taskCollection'
import type {DateISO} from '@/types/DateISO'
import type {DateKebab} from '@/types/DateKebab'
+import type {IProjectView} from '@/modelTypes/IProjectView'
// convenient internal filter object
export interface GanttFilters {
projectId: IProject['id']
+ viewId: IProjectView['id'],
dateFrom: DateISO
dateTo: DateISO
showTasksWithoutDates: boolean
@@ -41,6 +43,7 @@ function ganttRouteToFilters(route: Partial): GanttFilt
const ganttRoute = route
return {
projectId: Number(ganttRoute.params?.projectId),
+ viewId: Number(ganttRoute.params?.viewId),
dateFrom: parseDateProp(ganttRoute.query?.dateFrom as DateKebab) || getDefaultDateFrom(),
dateTo: parseDateProp(ganttRoute.query?.dateTo as DateKebab) || getDefaultDateTo(),
showTasksWithoutDates: parseBooleanProp(ganttRoute.query?.showTasksWithoutDates as string) || DEFAULT_SHOW_TASKS_WITHOUT_DATES,
@@ -69,8 +72,11 @@ function ganttFiltersToRoute(filters: GanttFilters): RouteLocationRaw {
}
return {
- name: 'project.gantt',
- params: {projectId: filters.projectId},
+ name: 'project.view',
+ params: {
+ projectId: filters.projectId,
+ viewId: filters.viewId,
+ },
query,
}
}
@@ -88,7 +94,7 @@ export type UseGanttFiltersReturn =
ReturnType> &
ReturnType>
-export function useGanttFilters(route: Ref): UseGanttFiltersReturn {
+export function useGanttFilters(route: Ref, viewId: IProjectView['id']): UseGanttFiltersReturn {
const {
filters,
hasDefaultFilters,
@@ -98,7 +104,7 @@ export function useGanttFilters(route: Ref): UseGanttFi
ganttGetDefaultFilters,
ganttRouteToFilters,
ganttFiltersToRoute,
- ['project.gantt'],
+ ['project.view'],
)
const {
@@ -108,7 +114,7 @@ export function useGanttFilters(route: Ref): UseGanttFi
isLoading,
addTask,
updateTask,
- } = useGanttTaskList(filters, ganttFiltersToApiParams)
+ } = useGanttTaskList(filters, ganttFiltersToApiParams, viewId)
return {
filters,
diff --git a/frontend/src/views/project/helpers/useGanttTaskList.ts b/frontend/src/views/project/helpers/useGanttTaskList.ts
index f2a76c8d6..e616c2399 100644
--- a/frontend/src/views/project/helpers/useGanttTaskList.ts
+++ b/frontend/src/views/project/helpers/useGanttTaskList.ts
@@ -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 type {Filters} from '@/composables/useRouteFilters'
@@ -10,16 +10,15 @@ import TaskService from '@/services/task'
import TaskModel from '@/models/task'
import {error, success} from '@/message'
import {useAuthStore} from '@/stores/auth'
+import type {IProjectView} from '@/modelTypes/IProjectView'
// FIXME: unify with general `useTaskList`
export function useGanttTaskList(
filters: Ref,
filterToApiParams: (filters: F) => TaskFilterParams,
- options: {
- loadAll?: boolean,
- } = {
- loadAll: true,
- }) {
+ viewId: IProjectView['id'],
+ loadAll: boolean = true,
+) {
const taskCollectionService = shallowReactive(new TaskCollectionService())
const taskService = shallowReactive(new TaskService())
const authStore = useAuthStore()
@@ -29,13 +28,13 @@ export function useGanttTaskList(
const tasks = ref