feat: rename list to project everywhere

fix: project table view

fix: e2e tests

fix: typo in readme

fix: list view route

fix: don't wait until background is loaded for list to show

fix: rename component imports

fix: lint

fix: parse task text

fix: use list card grid

fix: use correct class names

fix: i18n keys

fix: load project

fix: task overview

fix: list view spacing

fix: find project

fix: setLoading when updating a project

fix: loading saved filter

fix: project store loading

fix: color picker import

fix: cypress tests

feat: migrate old list settings

chore: add const for project settings

fix: wrong projecten rename from lists

chore: rename unused variable

fix: editor list

fix: shortcut list class name

fix: pagination list class name

fix: notifications list class name

fix: list view variable name

chore: clarify comment

fix: i18n keys

fix: router imports

fix: comment

chore: remove debugging leftover

fix: remove duplicate variables

fix: change comment

fix: list view variable name

fix: list view css class name

fix: list item property name

fix: name update tasks function correctly

fix: update comment

fix: project create route

fix: list view class names

fix: list view component name

fix: result list class name

fix: animation class list name

fix: change debug log

fix: revert a few navigation changes

fix: use @ for imports of all views

fix: rename link share list class

fix: remove unused css class

fix: dynamically import project components again
This commit is contained in:
kolaente 2022-11-13 22:04:57 +01:00 committed by Gitea
parent b9d3b5c756
commit befa6f27bb
133 changed files with 1873 additions and 1881 deletions

View File

@ -1,18 +1,18 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {NamespaceFactory} from '../../factories/namespace' import {NamespaceFactory} from '../../factories/namespace'
import {UserListFactory} from '../../factories/users_list' import {UserProjectFactory} from '../../factories/users_project'
describe('Editor', () => { describe('Editor', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
beforeEach(() => { beforeEach(() => {
NamespaceFactory.create(1) NamespaceFactory.create(1)
ListFactory.create(1) ProjectFactory.create(1)
TaskFactory.truncate() TaskFactory.truncate()
UserListFactory.truncate() UserProjectFactory.truncate()
}) })
it('Has a preview with checkable checkboxes', () => { it('Has a preview with checkable checkboxes', () => {

View File

@ -1,6 +1,6 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {NamespaceFactory} from '../../factories/namespace' import {NamespaceFactory} from '../../factories/namespace'
describe('Namepaces', () => { describe('Namepaces', () => {
@ -10,7 +10,7 @@ describe('Namepaces', () => {
beforeEach(() => { beforeEach(() => {
namespaces = NamespaceFactory.create(1) namespaces = NamespaceFactory.create(1)
ListFactory.create(1) ProjectFactory.create(1)
}) })
it('Should be all there', () => { it('Should be all there', () => {
@ -99,17 +99,17 @@ describe('Namepaces', () => {
.should('not.contain', newNamespaces[0].title) .should('not.contain', newNamespaces[0].title)
}) })
it('Should not show archived lists & namespaces if the filter is not checked', () => { it('Should not show archived projects & namespaces if the filter is not checked', () => {
const n = NamespaceFactory.create(1, { const n = NamespaceFactory.create(1, {
id: 2, id: 2,
is_archived: true, is_archived: true,
}, false) }, false)
ListFactory.create(1, { ProjectFactory.create(1, {
id: 2, id: 2,
namespace_id: n[0].id, namespace_id: n[0].id,
}, false) }, false)
ListFactory.create(1, { ProjectFactory.create(1, {
id: 3, id: 3,
is_archived: true, is_archived: true,
}, false) }, false)

View File

@ -1,19 +1,19 @@
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {NamespaceFactory} from '../../factories/namespace' import {NamespaceFactory} from '../../factories/namespace'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
export function createLists() { export function createProjects() {
NamespaceFactory.create(1) NamespaceFactory.create(1)
const lists = ListFactory.create(1, { const projects = ProjectFactory.create(1, {
title: 'First List' title: 'First Project'
}) })
TaskFactory.truncate() TaskFactory.truncate()
return lists return projects
} }
export function prepareLists(setLists = (...args: any[]) => {}) { export function prepareProjects(setProjects = (...args: any[]) => {}) {
beforeEach(() => { beforeEach(() => {
const lists = createLists() const projects = createProjects()
setLists(lists) setProjects(projects)
}) })
} }

View File

@ -1,41 +1,41 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {prepareLists} from './prepareLists' import {prepareProjects} from './prepareProjects'
describe('List History', () => { describe('Project History', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
prepareLists() prepareProjects()
it('should show a list history on the home page', () => { it('should show a project history on the home page', () => {
cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces') cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces')
cy.intercept(Cypress.env('API_URL') + '/lists/*').as('loadList') cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
const lists = ListFactory.create(6) const projects = ProjectFactory.create(6)
cy.visit('/') cy.visit('/')
cy.wait('@loadNamespaces') cy.wait('@loadNamespaces')
cy.get('body') cy.get('body')
.should('not.contain', 'Last viewed') .should('not.contain', 'Last viewed')
cy.visit(`/lists/${lists[0].id}`) cy.visit(`/projects/${projects[0].id}`)
cy.wait('@loadNamespaces') cy.wait('@loadNamespaces')
cy.wait('@loadList') cy.wait('@loadProject')
cy.visit(`/lists/${lists[1].id}`) cy.visit(`/projects/${projects[1].id}`)
cy.wait('@loadNamespaces') cy.wait('@loadNamespaces')
cy.wait('@loadList') cy.wait('@loadProject')
cy.visit(`/lists/${lists[2].id}`) cy.visit(`/projects/${projects[2].id}`)
cy.wait('@loadNamespaces') cy.wait('@loadNamespaces')
cy.wait('@loadList') cy.wait('@loadProject')
cy.visit(`/lists/${lists[3].id}`) cy.visit(`/projects/${projects[3].id}`)
cy.wait('@loadNamespaces') cy.wait('@loadNamespaces')
cy.wait('@loadList') cy.wait('@loadProject')
cy.visit(`/lists/${lists[4].id}`) cy.visit(`/projects/${projects[4].id}`)
cy.wait('@loadNamespaces') cy.wait('@loadNamespaces')
cy.wait('@loadList') cy.wait('@loadProject')
cy.visit(`/lists/${lists[5].id}`) cy.visit(`/projects/${projects[5].id}`)
cy.wait('@loadNamespaces') cy.wait('@loadNamespaces')
cy.wait('@loadList') cy.wait('@loadProject')
// cy.visit('/') // cy.visit('/')
// cy.wait('@loadNamespaces') // cy.wait('@loadNamespaces')
@ -46,12 +46,12 @@ describe('List History', () => {
cy.get('body') cy.get('body')
.should('contain', 'Last viewed') .should('contain', 'Last viewed')
cy.get('[data-cy="listCardGrid"]') cy.get('[data-cy="projectCardGrid"]')
.should('not.contain', lists[0].title) .should('not.contain', projects[0].title)
.should('contain', lists[1].title) .should('contain', projects[1].title)
.should('contain', lists[2].title) .should('contain', projects[2].title)
.should('contain', lists[3].title) .should('contain', projects[3].title)
.should('contain', lists[4].title) .should('contain', projects[4].title)
.should('contain', lists[5].title) .should('contain', projects[5].title)
}) })
}) })

View File

@ -3,15 +3,15 @@ import {formatISO, format} from 'date-fns'
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {prepareLists} from './prepareLists' import {prepareProjects} from './prepareProjects'
describe('List View Gantt', () => { describe('Project View Gantt', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
prepareLists() prepareProjects()
it('Hides tasks with no dates', () => { it('Hides tasks with no dates', () => {
const tasks = TaskFactory.create(1) const tasks = TaskFactory.create(1)
cy.visit('/lists/1/gantt') cy.visit('/projects/1/gantt')
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('List View Gantt', () => {
nextMonth.setDate(1) nextMonth.setDate(1)
nextMonth.setMonth(9) nextMonth.setMonth(9)
cy.visit('/lists/1/gantt') cy.visit('/projects/1/gantt')
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('List 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('/lists/1/gantt') cy.visit('/projects/1/gantt')
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('List View Gantt', () => {
start_date: null, start_date: null,
end_date: null, end_date: null,
}) })
cy.visit('/lists/1/gantt') cy.visit('/projects/1/gantt')
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('List 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('/lists/1/gantt') cy.visit('/projects/1/gantt')
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,9 +83,9 @@ describe('List 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('/lists/1/gantt') cy.visit('/projects/1/gantt')
cy.get('.list-gantt .gantt-options .field .control input.input.form-control') cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
.click() .click()
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day') cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
.first() .first()
@ -99,13 +99,13 @@ describe('List 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('/lists/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05') cy.visit('/projects/1/gantt?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')
.should('contain', 'October 2022') .should('contain', 'October 2022')
.should('contain', 'November 2022') .should('contain', 'November 2022')
cy.get('.list-gantt .gantt-options .field .control input.input.form-control') cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
.should('have.value', '25 Sep 2022 to 5 Nov 2022') .should('have.value', '25 Sep 2022 to 5 Nov 2022')
}) })
@ -115,7 +115,7 @@ describe('List 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('/lists/1/gantt') cy.visit('/projects/1/gantt')
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()

View File

@ -1,13 +1,13 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {BucketFactory} from '../../factories/bucket' import {BucketFactory} from '../../factories/bucket'
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {prepareLists} from './prepareLists' import {prepareProjects} from './prepareProjects'
describe('List View Kanban', () => { describe('Project View Kanban', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
prepareLists() prepareProjects()
let buckets let buckets
beforeEach(() => { beforeEach(() => {
@ -16,10 +16,10 @@ describe('List View Kanban', () => {
it('Shows all buckets with their tasks', () => { it('Shows all buckets with their tasks', () => {
const data = TaskFactory.create(10, { const data = TaskFactory.create(10, {
list_id: 1, project_id: 1,
bucket_id: 1, bucket_id: 1,
}) })
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .title') cy.get('.kanban .bucket .title')
.contains(buckets[0].title) .contains(buckets[0].title)
@ -34,10 +34,10 @@ describe('List View Kanban', () => {
it('Can add a new task to a bucket', () => { it('Can add a new task to a bucket', () => {
TaskFactory.create(2, { TaskFactory.create(2, {
list_id: 1, project_id: 1,
bucket_id: 1, bucket_id: 1,
}) })
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket') cy.get('.kanban .bucket')
.contains(buckets[0].title) .contains(buckets[0].title)
@ -55,7 +55,7 @@ describe('List View Kanban', () => {
}) })
it('Can create a new bucket', () => { it('Can create a new bucket', () => {
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket.new-bucket .button') cy.get('.kanban .bucket.new-bucket .button')
.click() .click()
@ -69,7 +69,7 @@ describe('List View Kanban', () => {
}) })
it('Can set a bucket limit', () => { it('Can set a bucket limit', () => {
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger') cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
.first() .first()
@ -90,7 +90,7 @@ describe('List View Kanban', () => {
}) })
it('Can rename a bucket', () => { it('Can rename a bucket', () => {
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .bucket-header .title') cy.get('.kanban .bucket .bucket-header .title')
.first() .first()
@ -101,7 +101,7 @@ describe('List View Kanban', () => {
}) })
it('Can delete a bucket', () => { it('Can delete a bucket', () => {
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger') cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
.first() .first()
@ -125,10 +125,10 @@ describe('List View Kanban', () => {
it('Can drag tasks around', () => { it('Can drag tasks around', () => {
const tasks = TaskFactory.create(2, { const tasks = TaskFactory.create(2, {
list_id: 1, project_id: 1,
bucket_id: 1, bucket_id: 1,
}) })
cy.visit('/lists/1/kanban') 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)
@ -144,10 +144,10 @@ describe('List 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 = TaskFactory.create(5, {
id: '{increment}', id: '{increment}',
list_id: 1, project_id: 1,
bucket_id: 1, bucket_id: 1,
}) })
cy.visit('/lists/1/kanban') 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)
@ -158,18 +158,18 @@ describe('List View Kanban', () => {
.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 list', () => { it('Should remove a task from the kanban board when moving it to another project', () => {
const lists = ListFactory.create(2) const projects = ProjectFactory.create(2)
BucketFactory.create(2, { BucketFactory.create(2, {
list_id: '{increment}', project_id: '{increment}',
}) })
const tasks = TaskFactory.create(5, { const tasks = TaskFactory.create(5, {
id: '{increment}', id: '{increment}',
list_id: 1, project_id: 1,
bucket_id: 1, bucket_id: 1,
}) })
const task = tasks[0] const task = tasks[0]
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .tasks .task') cy.get('.kanban .bucket .tasks .task')
.contains(task.title) .contains(task.title)
@ -180,7 +180,7 @@ describe('List View Kanban', () => {
.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')
.type(`${lists[1].title}{enter}`) .type(`${projects[1].title}{enter}`)
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress // The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
// presses enter and we can't simulate pressing on enter to select the item. // presses enter and we can't simulate pressing on enter to select the item.
cy.get('.task-view .content.details .field .multiselect.control .search-results') cy.get('.task-view .content.details .field .multiselect.control .search-results')
@ -197,26 +197,26 @@ describe('List 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, { const data = TaskFactory.create(10, {
list_id: 1, project_id: 1,
bucket_id: 1, bucket_id: 1,
}) })
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.list-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 lists = ListFactory.create(1) const projects = ProjectFactory.create(1)
const buckets = BucketFactory.create(2, { const buckets = BucketFactory.create(2, {
list_id: lists[0].id, project_id: projects[0].id,
}) })
const tasks = TaskFactory.create(5, { const tasks = TaskFactory.create(5, {
list_id: 1, project_id: 1,
bucket_id: buckets[0].id, bucket_id: buckets[0].id,
}) })
const task = tasks[0] const task = tasks[0]
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .tasks .task') cy.get('.kanban .bucket .tasks .task')
.contains(task.title) .contains(task.title)

View File

@ -1,32 +1,32 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {UserListFactory} from '../../factories/users_list' import {UserProjectFactory} from '../../factories/users_project'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {UserFactory} from '../../factories/user' import {UserFactory} from '../../factories/user'
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {prepareLists} from './prepareLists' import {prepareProjects} from './prepareProjects'
describe('List View List', () => { describe('Project View Project', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
prepareLists() prepareProjects()
it('Should be an empty list', () => { it('Should be an empty project', () => {
cy.visit('/lists/1') cy.visit('/projects/1')
cy.url() cy.url()
.should('contain', '/lists/1/list') .should('contain', '/projects/1/list')
cy.get('.list-title') cy.get('.project-title')
.should('contain', 'First List') .should('contain', 'First Project')
cy.get('.list-title-dropdown') cy.get('.project-title-dropdown')
.should('exist') .should('exist')
cy.get('p') cy.get('p')
.contains('This list is currently empty.') .contains('This project is currently empty.')
.should('exist') .should('exist')
}) })
it('Should create a new task', () => { it('Should create a new task', () => {
const newTaskTitle = 'New task' const newTaskTitle = 'New task'
cy.visit('/lists/1') cy.visit('/projects/1')
cy.get('.task-add textarea') cy.get('.task-add textarea')
.type(newTaskTitle+'{enter}') .type(newTaskTitle+'{enter}')
cy.get('.tasks') cy.get('.tasks')
@ -36,9 +36,9 @@ describe('List View List', () => {
it('Should navigate to the task when the title is clicked', () => { it('Should navigate to the task when the title is clicked', () => {
const tasks = TaskFactory.create(5, { const tasks = TaskFactory.create(5, {
id: '{increment}', id: '{increment}',
list_id: 1, project_id: 1,
}) })
cy.visit('/lists/1/list') cy.visit('/projects/1/list')
cy.get('.tasks .task .tasktext') cy.get('.tasks .task .tasktext')
.contains(tasks[0].title) .contains(tasks[0].title)
@ -49,33 +49,33 @@ describe('List View List', () => {
.should('contain', `/tasks/${tasks[0].id}`) .should('contain', `/tasks/${tasks[0].id}`)
}) })
it('Should not see any elements for a list which is shared read only', () => { it('Should not see any elements for a project which is shared read only', () => {
UserFactory.create(2) UserFactory.create(2)
UserListFactory.create(1, { UserProjectFactory.create(1, {
list_id: 2, project_id: 2,
user_id: 1, user_id: 1,
right: 0, right: 0,
}) })
const lists = ListFactory.create(2, { const projects = ProjectFactory.create(2, {
owner_id: '{increment}', owner_id: '{increment}',
namespace_id: '{increment}', namespace_id: '{increment}',
}) })
cy.visit(`/lists/${lists[1].id}/`) cy.visit(`/projects/${projects[1].id}/`)
cy.get('.list-title-wrapper .icon') cy.get('.project-title-wrapper .icon')
.should('not.exist') .should('not.exist')
cy.get('input.input[placeholder="Add a new task..."') cy.get('input.input[placeholder="Add a new task..."')
.should('not.exist') .should('not.exist')
}) })
it('Should only show the color of a list in the navigation and not in the list view', () => { it('Should only show the color of a project in the navigation and not in the project view', () => {
const lists = ListFactory.create(1, { const projects = ProjectFactory.create(1, {
hex_color: '00db60', hex_color: '00db60',
}) })
TaskFactory.create(10, { TaskFactory.create(10, {
list_id: lists[0].id, project_id: projects[0].id,
}) })
cy.visit(`/lists/${lists[0].id}/`) cy.visit(`/projects/${projects[0].id}/`)
cy.get('.menu-list li .list-menu-link .color-bubble') cy.get('.menu-list li .list-menu-link .color-bubble')
.should('have.css', 'background-color', 'rgb(0, 219, 96)') .should('have.css', 'background-color', 'rgb(0, 219, 96)')
@ -87,9 +87,9 @@ describe('List View List', () => {
const tasks = TaskFactory.create(100, { const tasks = TaskFactory.create(100, {
id: '{increment}', id: '{increment}',
title: i => `task${i}`, title: i => `task${i}`,
list_id: 1, project_id: 1,
}) })
cy.visit('/lists/1/list') cy.visit('/projects/1/list')
cy.get('.tasks') cy.get('.tasks')
.should('contain', tasks[1].title) .should('contain', tasks[1].title)

View File

@ -2,37 +2,37 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
describe('List View Table', () => { describe('Project View Table', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
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('/lists/1/table') cy.visit('/projects/1/table')
cy.get('.list-table table.table') cy.get('.project-table table.table')
.should('exist') .should('exist')
cy.get('.list-table table.table') cy.get('.project-table table.table')
.should('contain', tasks[0].title) .should('contain', tasks[0].title)
}) })
it('Should have working column switches', () => { it('Should have working column switches', () => {
TaskFactory.create(1) TaskFactory.create(1)
cy.visit('/lists/1/table') cy.visit('/projects/1/table')
cy.get('.list-table .filter-container .items .button') cy.get('.project-table .filter-container .items .button')
.contains('Columns') .contains('Columns')
.click() .click()
cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check') cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
.contains('Priority') .contains('Priority')
.click() .click()
cy.get('.list-table .filter-container .card.columns-filter .card-content .fancycheckbox .check') cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox .check')
.contains('Done') .contains('Done')
.click() .click()
cy.get('.list-table table.table th') cy.get('.project-table table.table th')
.contains('Priority') .contains('Priority')
.should('exist') .should('exist')
cy.get('.list-table table.table th') cy.get('.project-table table.table th')
.contains('Done') .contains('Done')
.should('not.exist') .should('not.exist')
}) })
@ -40,11 +40,11 @@ describe('List View Table', () => {
it('Should navigate to the task when the title is clicked', () => { it('Should navigate to the task when the title is clicked', () => {
const tasks = TaskFactory.create(5, { const tasks = TaskFactory.create(5, {
id: '{increment}', id: '{increment}',
list_id: 1, project_id: 1,
}) })
cy.visit('/lists/1/table') cy.visit('/projects/1/table')
cy.get('.list-table table.table') cy.get('.project-table table.table')
.contains(tasks[0].title) .contains(tasks[0].title)
.click() .click()

View File

@ -1,58 +1,58 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {prepareLists} from './prepareLists' import {prepareProjects} from './prepareProjects'
describe('Lists', () => { describe('Projects', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
let lists let projects
prepareLists((newLists) => (lists = newLists)) prepareProjects((newProjects) => (projects = newProjects))
it('Should create a new list', () => { it('Should create a new project', () => {
cy.visit('/') cy.visit('/')
cy.get('.namespace-title .dropdown-trigger') cy.get('.namespace-title .dropdown-trigger')
.click() .click()
cy.get('.namespace-title .dropdown .dropdown-item') cy.get('.namespace-title .dropdown .dropdown-item')
.contains('New list') .contains('New project')
.click() .click()
cy.url() cy.url()
.should('contain', '/lists/new/1') .should('contain', '/projects/new/1')
cy.get('.card-header-title') cy.get('.card-header-title')
.contains('New list') .contains('New project')
cy.get('input.input') cy.get('input.input')
.type('New List') .type('New Project')
cy.get('.button') cy.get('.button')
.contains('Create') .contains('Create')
.click() .click()
cy.get('.global-notification', { timeout: 1000 }) // Waiting until the request to create the new list is done cy.get('.global-notification', { timeout: 1000 }) // Waiting until the request to create the new project is done
.should('contain', 'Success') .should('contain', 'Success')
cy.url() cy.url()
.should('contain', '/lists/') .should('contain', '/projects/')
cy.get('.list-title') cy.get('.project-title')
.should('contain', 'New List') .should('contain', 'New Project')
}) })
it('Should redirect to a specific list view after visited', () => { it('Should redirect to a specific project view after visited', () => {
cy.visit('/lists/1/kanban') cy.visit('/projects/1/kanban')
cy.url() cy.url()
.should('contain', '/lists/1/kanban') .should('contain', '/projects/1/kanban')
cy.visit('/lists/1') cy.visit('/projects/1')
cy.url() cy.url()
.should('contain', '/lists/1/kanban') .should('contain', '/projects/1/kanban')
}) })
it('Should rename the list in all places', () => { it('Should rename the project in all places', () => {
TaskFactory.create(5, { TaskFactory.create(5, {
id: '{increment}', id: '{increment}',
list_id: 1, project_id: 1,
}) })
const newListName = 'New list name' const newProjectName = 'New project name'
cy.visit('/lists/1') cy.visit('/projects/1')
cy.get('.list-title') cy.get('.project-title')
.should('contain', 'First List') .should('contain', 'First Project')
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger') cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
.click() .click()
@ -60,27 +60,27 @@ describe('Lists', () => {
.contains('Edit') .contains('Edit')
.click() .click()
cy.get('#title') cy.get('#title')
.type(`{selectall}${newListName}`) .type(`{selectall}${newProjectName}`)
cy.get('footer.card-footer .button') cy.get('footer.card-footer .button')
.contains('Save') .contains('Save')
.click() .click()
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')
cy.get('.list-title') cy.get('.project-title')
.should('contain', newListName) .should('contain', newProjectName)
.should('not.contain', lists[0].title) .should('not.contain', projects[0].title)
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child') cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
.should('contain', newListName) .should('contain', newProjectName)
.should('not.contain', lists[0].title) .should('not.contain', projects[0].title)
cy.visit('/') cy.visit('/')
cy.get('.card-content') cy.get('.card-content')
.should('contain', newListName) .should('contain', newProjectName)
.should('not.contain', lists[0].title) .should('not.contain', projects[0].title)
}) })
it('Should remove a list', () => { it('Should remove a project', () => {
cy.visit(`/lists/${lists[0].id}`) cy.visit(`/projects/${projects[0].id}`)
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger') cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
.click() .click()
@ -96,27 +96,27 @@ describe('Lists', () => {
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')
cy.get('.namespace-container .menu.namespaces-lists .menu-list') cy.get('.namespace-container .menu.namespaces-lists .menu-list')
.should('not.contain', lists[0].title) .should('not.contain', projects[0].title)
cy.location('pathname') cy.location('pathname')
.should('equal', '/') .should('equal', '/')
}) })
it('Should archive a list', () => { it('Should archive a project', () => {
cy.visit(`/lists/${lists[0].id}`) cy.visit(`/projects/${projects[0].id}`)
cy.get('.list-title-dropdown') cy.get('.project-title-dropdown')
.click() .click()
cy.get('.list-title-dropdown .dropdown-menu .dropdown-item') cy.get('.project-title-dropdown .dropdown-menu .dropdown-item')
.contains('Archive') .contains('Archive')
.click() .click()
cy.get('.modal-content') cy.get('.modal-content')
.should('contain.text', 'Archive this list') .should('contain.text', 'Archive this project')
cy.get('.modal-content [data-cy=modalPrimary]') cy.get('.modal-content [data-cy=modalPrimary]')
.click() .click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list') cy.get('.namespace-container .menu.namespaces-lists .menu-list')
.should('not.contain', lists[0].title) .should('not.contain', projects[0].title)
cy.get('main.app-content') cy.get('main.app-content')
.should('contain.text', 'This list is archived. It is not possible to create new or edit tasks for it.') .should('contain.text', 'This project is archived. It is not possible to create new or edit tasks for it.')
}) })
}) })

View File

@ -1,22 +1,22 @@
import {LinkShareFactory} from '../../factories/link_sharing' import {LinkShareFactory} from '../../factories/link_sharing'
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
describe('Link shares', () => { describe('Link shares', () => {
it('Can view a link share', () => { it('Can view a link share', () => {
const lists = ListFactory.create(1) const projects = ProjectFactory.create(1)
const tasks = TaskFactory.create(10, { const tasks = TaskFactory.create(10, {
list_id: lists[0].id project_id: projects[0].id
}) })
const linkShares = LinkShareFactory.create(1, { const linkShares = LinkShareFactory.create(1, {
list_id: lists[0].id, project_id: projects[0].id,
right: 0, right: 0,
}) })
cy.visit(`/share/${linkShares[0].hash}/auth`) cy.visit(`/share/${linkShares[0].hash}/auth`)
cy.get('h1.title') cy.get('h1.title')
.should('contain', lists[0].title) .should('contain', projects[0].title)
cy.get('input.input[placeholder="Add a new task..."') cy.get('input.input[placeholder="Add a new task..."')
.should('not.exist') .should('not.exist')
cy.get('.tasks') cy.get('.tasks')

View File

@ -1,6 +1,6 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {seed} from '../../support/seed' import {seed} from '../../support/seed'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {NamespaceFactory} from '../../factories/namespace' import {NamespaceFactory} from '../../factories/namespace'
@ -9,9 +9,9 @@ import {updateUserSettings} from '../../support/updateUserSettings'
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) { function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
NamespaceFactory.create(1) NamespaceFactory.create(1)
const list = ListFactory.create()[0] const project = ProjectFactory.create()[0]
BucketFactory.create(1, { BucketFactory.create(1, {
list_id: list.id, project_id: project.id,
}) })
const tasks = [] const tasks = []
let dueDate = startDueDate let dueDate = startDueDate
@ -20,7 +20,7 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2)) dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
tasks.push({ tasks.push({
id: i + 1, id: i + 1,
list_id: list.id, project_id: project.id,
done: false, done: false,
created_by_id: 1, created_by_id: 1,
title: 'Test Task ' + i, title: 'Test Task ' + i,
@ -31,7 +31,7 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
}) })
} }
seed(TaskFactory.table, tasks) seed(TaskFactory.table, tasks)
return {tasks, list} return {tasks, project}
} }
describe('Home Page Task Overview', () => { describe('Home Page Task Overview', () => {
@ -73,7 +73,7 @@ describe('Home Page Task Overview', () => {
due_date: new Date().toISOString(), due_date: new Date().toISOString(),
}, false) }, false)
cy.visit(`/lists/${tasks[0].list_id}/list`) cy.visit(`/projects/${tasks[0].project_id}/list`)
cy.get('.tasks .task') cy.get('.tasks .task')
.first() .first()
.should('contain.text', newTaskTitle) .should('contain.text', newTaskTitle)
@ -90,7 +90,7 @@ describe('Home Page Task Overview', () => {
cy.visit('/') cy.visit('/')
cy.visit(`/lists/${tasks[0].list_id}/list`) cy.visit(`/projects/${tasks[0].project_id}/list`)
cy.get('.task-add textarea') cy.get('.task-add textarea')
.type(newTaskTitle+'{enter}') .type(newTaskTitle+'{enter}')
cy.visit('/') cy.visit('/')
@ -113,10 +113,10 @@ describe('Home Page Task Overview', () => {
.should('contain.text', newTaskTitle) .should('contain.text', newTaskTitle)
}) })
it('Should show a task without a due date added via default list at the bottom', () => { it('Should show a task without a due date added via default project at the bottom', () => {
const {list} = seedTasks(40) const {project} = seedTasks(40)
updateUserSettings({ updateUserSettings({
default_list_id: list.id, default_project_id: project.id,
overdue_tasks_reminders_time: '9:00', overdue_tasks_reminders_time: '9:00',
}) })
@ -131,23 +131,23 @@ describe('Home Page Task Overview', () => {
.should('contain.text', newTaskTitle) .should('contain.text', newTaskTitle)
}) })
it('Should show the cta buttons for new list when there are no tasks', () => { it('Should show the cta buttons for new project when there are no tasks', () => {
TaskFactory.truncate() TaskFactory.truncate()
cy.visit('/') cy.visit('/')
cy.get('.home.app-content .content') cy.get('.home.app-content .content')
.should('contain.text', 'You can create a new list for your new tasks:') .should('contain.text', 'You can create a new project for your new tasks:')
.should('contain.text', 'Or import your lists and tasks from other services into Vikunja:') .should('contain.text', 'Or import your projects and tasks from other services into Vikunja:')
}) })
it('Should not show the cta buttons for new list when there are tasks', () => { it('Should not show the cta buttons for new project when there are tasks', () => {
seedTasks() seedTasks()
cy.visit('/') cy.visit('/')
cy.get('.home.app-content .content') cy.get('.home.app-content .content')
.should('not.contain.text', 'You can create a new list for your new tasks:') .should('not.contain.text', 'You can create a new project for your new tasks:')
.should('not.contain.text', 'Or import your lists and tasks from other services into Vikunja:') .should('not.contain.text', 'Or import your projects and tasks from other services into Vikunja:')
}) })
}) })

View File

@ -1,11 +1,11 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {ListFactory} from '../../factories/list' import {ProjectFactory} from '../../factories/project'
import {TaskCommentFactory} from '../../factories/task_comment' import {TaskCommentFactory} from '../../factories/task_comment'
import {UserFactory} from '../../factories/user' import {UserFactory} from '../../factories/user'
import {NamespaceFactory} from '../../factories/namespace' import {NamespaceFactory} from '../../factories/namespace'
import {UserListFactory} from '../../factories/users_list' import {UserProjectFactory} from '../../factories/users_project'
import {TaskAssigneeFactory} from '../../factories/task_assignee' import {TaskAssigneeFactory} from '../../factories/task_assignee'
import {LabelFactory} from '../../factories/labels' import {LabelFactory} from '../../factories/labels'
import {LabelTaskFactory} from '../../factories/label_task' import {LabelTaskFactory} from '../../factories/label_task'
@ -48,22 +48,22 @@ describe('Task', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
let namespaces let namespaces
let lists let projects
let buckets let buckets
beforeEach(() => { beforeEach(() => {
// UserFactory.create(1) // UserFactory.create(1)
namespaces = NamespaceFactory.create(1) namespaces = NamespaceFactory.create(1)
lists = ListFactory.create(1) projects = ProjectFactory.create(1)
buckets = BucketFactory.create(1, { buckets = BucketFactory.create(1, {
list_id: lists[0].id, project_id: projects[0].id,
}) })
TaskFactory.truncate() TaskFactory.truncate()
UserListFactory.truncate() UserProjectFactory.truncate()
}) })
it('Should be created new', () => { it('Should be created new', () => {
cy.visit('/lists/1/list') cy.visit('/projects/1/list')
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')
@ -74,11 +74,11 @@ describe('Task', () => {
.should('contain', 'New Task') .should('contain', 'New Task')
}) })
it('Inserts new tasks at the top of the list', () => { it('Inserts new tasks at the top of the project', () => {
TaskFactory.create(1) TaskFactory.create(1)
cy.visit('/lists/1/list') cy.visit('/projects/1/list')
cy.get('.list-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…"')
.type('New Task') .type('New Task')
@ -95,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('/lists/1/list') cy.visit('/projects/1/list')
cy.get('.tasks .task .fancycheckbox label.check') cy.get('.tasks .task .fancycheckbox label.check')
.first() .first()
.click() .click()
@ -106,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('/lists/1/list') cy.visit('/projects/1/list')
cy.get('.tasks .task .favorite') cy.get('.tasks .task .favorite')
.first() .first()
.click() .click()
@ -134,7 +134,7 @@ describe('Task', () => {
.should('contain', '#1') .should('contain', '#1')
cy.get('.task-view h6.subtitle') cy.get('.task-view h6.subtitle')
.should('contain', namespaces[0].title) .should('contain', namespaces[0].title)
.should('contain', lists[0].title) .should('contain', projects[0].title)
cy.get('.task-view .details.content.description') cy.get('.task-view .details.content.description')
.should('contain', tasks[0].description) .should('contain', tasks[0].description)
cy.get('.task-view .action-buttons p.created') cy.get('.task-view .action-buttons p.created')
@ -179,21 +179,21 @@ describe('Task', () => {
.should('contain', 'Mark as undone') .should('contain', 'Mark as undone')
}) })
it('Shows a task identifier since the list has one', () => { it('Shows a task identifier since the project has one', () => {
const lists = ListFactory.create(1, { const projects = ProjectFactory.create(1, {
id: 1, id: 1,
identifier: 'TEST', identifier: 'TEST',
}) })
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: lists[0].id, project_id: projects[0].id,
index: 1, index: 1,
}) })
cy.visit(`/tasks/${tasks[0].id}`) cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view h1.title.task-id') cy.get('.task-view h1.title.task-id')
.should('contain', `${lists[0].identifier}-${tasks[0].index}`) .should('contain', `${projects[0].identifier}-${tasks[0].index}`)
}) })
it('Can edit the description', () => { it('Can edit the description', () => {
@ -236,14 +236,14 @@ describe('Task', () => {
.should('contain', 'Success') .should('contain', 'Success')
}) })
it('Can move a task to another list', () => { it('Can move a task to another project', () => {
const lists = ListFactory.create(2) const projects = ProjectFactory.create(2)
BucketFactory.create(2, { BucketFactory.create(2, {
list_id: '{increment}' project_id: '{increment}'
}) })
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: lists[0].id, project_id: projects[0].id,
}) })
cy.visit(`/tasks/${tasks[0].id}`) cy.visit(`/tasks/${tasks[0].id}`)
@ -251,7 +251,7 @@ describe('Task', () => {
.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')
.type(`${lists[1].title}{enter}`) .type(`${projects[1].title}{enter}`)
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress // The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
// presses enter and we can't simulate pressing on enter to select the item. // presses enter and we can't simulate pressing on enter to select the item.
cy.get('.task-view .content.details .field .multiselect.control .search-results') cy.get('.task-view .content.details .field .multiselect.control .search-results')
@ -261,7 +261,7 @@ describe('Task', () => {
cy.get('.task-view h6.subtitle') cy.get('.task-view h6.subtitle')
.should('contain', namespaces[0].title) .should('contain', namespaces[0].title)
.should('contain', lists[1].title) .should('contain', projects[1].title)
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')
}) })
@ -269,7 +269,7 @@ describe('Task', () => {
it('Can delete a task', () => { it('Can delete a task', () => {
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: 1, project_id: 1,
}) })
cy.visit(`/tasks/${tasks[0].id}`) cy.visit(`/tasks/${tasks[0].id}`)
@ -286,17 +286,17 @@ describe('Task', () => {
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')
cy.url() cy.url()
.should('contain', `/lists/${tasks[0].list_id}/`) .should('contain', `/projects/${tasks[0].project_id}/`)
}) })
it('Can add an assignee to a task', () => { it('Can add an assignee to a task', () => {
const users = UserFactory.create(5) const users = UserFactory.create(5)
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: 1, project_id: 1,
}) })
UserListFactory.create(5, { UserProjectFactory.create(5, {
list_id: 1, project_id: 1,
user_id: '{increment}', user_id: '{increment}',
}) })
@ -321,10 +321,10 @@ describe('Task', () => {
const users = UserFactory.create(2) const users = UserFactory.create(2)
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: 1, project_id: 1,
}) })
UserListFactory.create(5, { UserProjectFactory.create(5, {
list_id: 1, project_id: 1,
user_id: '{increment}', user_id: '{increment}',
}) })
TaskAssigneeFactory.create(1, { TaskAssigneeFactory.create(1, {
@ -347,7 +347,7 @@ describe('Task', () => {
it('Can add a new label to a task', () => { it('Can add a new label to a task', () => {
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: 1, project_id: 1,
}) })
LabelFactory.truncate() LabelFactory.truncate()
const newLabelText = 'some new label' const newLabelText = 'some new label'
@ -375,7 +375,7 @@ describe('Task', () => {
it('Can add an existing label to a task', () => { it('Can add an existing label to a task', () => {
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: 1, project_id: 1,
}) })
const labels = LabelFactory.create(1) const labels = LabelFactory.create(1)
LabelTaskFactory.truncate() LabelTaskFactory.truncate()
@ -388,13 +388,13 @@ describe('Task', () => {
it('Can add a label to a task and it shows up on the kanban board afterwards', () => { it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: lists[0].id, project_id: projects[0].id,
bucket_id: buckets[0].id, bucket_id: buckets[0].id,
}) })
const labels = LabelFactory.create(1) const labels = LabelFactory.create(1)
LabelTaskFactory.truncate() LabelTaskFactory.truncate()
cy.visit(`/lists/${lists[0].id}/kanban`) cy.visit(`/projects/${projects[0].id}/kanban`)
cy.get('.bucket .task') cy.get('.bucket .task')
.contains(tasks[0].title) .contains(tasks[0].title)
@ -412,7 +412,7 @@ describe('Task', () => {
it('Can remove a label from a task', () => { it('Can remove a label from a task', () => {
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: 1, project_id: 1,
}) })
const labels = LabelFactory.create(1) const labels = LabelFactory.create(1)
LabelTaskFactory.create(1, { LabelTaskFactory.create(1, {
@ -527,13 +527,13 @@ describe('Task', () => {
TaskAttachmentFactory.truncate() TaskAttachmentFactory.truncate()
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
id: 1, id: 1,
list_id: lists[0].id, project_id: projects[0].id,
bucket_id: buckets[0].id, bucket_id: buckets[0].id,
}) })
const labels = LabelFactory.create(1) const labels = LabelFactory.create(1)
LabelTaskFactory.truncate() LabelTaskFactory.truncate()
cy.visit(`/lists/${lists[0].id}/kanban`) cy.visit(`/projects/${projects[0].id}/kanban`)
cy.get('.bucket .task') cy.get('.bucket .task')
.contains(tasks[0].title) .contains(tasks[0].title)

View File

@ -1,5 +1,5 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {createLists} from '../list/prepareLists' import {createProjects} from '../project/prepareProjects'
function logout() { function logout() {
cy.get('.navbar .username-dropdown-trigger') cy.get('.navbar .username-dropdown-trigger')
@ -26,21 +26,21 @@ describe('Log out', () => {
}) })
}) })
it.skip('Should clear the list history after logging the user out', () => { it.skip('Should clear the project history after logging the user out', () => {
const lists = createLists() const projects = createProjects()
cy.visit(`/lists/${lists[0].id}`) cy.visit(`/projects/${projects[0].id}`)
.then(() => { .then(() => {
expect(localStorage.getItem('listHistory')).to.not.eq(null) expect(localStorage.getItem('projectHistory')).to.not.eq(null)
}) })
logout() logout()
cy.wait(1000) // This makes re-loading of the list and associated entities (and the resulting error) visible cy.wait(1000) // This makes re-loading of the project and associated entities (and the resulting error) visible
cy.url() cy.url()
.should('contain', '/login') .should('contain', '/login')
.then(() => { .then(() => {
expect(localStorage.getItem('listHistory')).to.eq(null) expect(localStorage.getItem('projectHistory')).to.eq(null)
}) })
}) })
}) })

View File

@ -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),
list_id: 1, project_id: 1,
created_by_id: 1, created_by_id: 1,
created: now.toISOString(), created: now.toISOString(),
updated: now.toISOString(), updated: now.toISOString(),

View File

@ -10,7 +10,7 @@ export class LinkShareFactory extends Factory {
return { return {
id: '{increment}', id: '{increment}',
hash: faker.random.word(32), hash: faker.random.word(32),
list_id: 1, project_id: 1,
right: 0, right: 0,
sharing_type: 0, sharing_type: 0,
shared_by_id: 1, shared_by_id: 1,

View File

@ -1,8 +1,8 @@
import {Factory} from '../support/factory' import {Factory} from '../support/factory'
import {faker} from '@faker-js/faker' import {faker} from '@faker-js/faker'
export class ListFactory extends Factory { export class ProjectFactory extends Factory {
static table = 'lists' static table = 'projects'
static factory() { static factory() {
const now = new Date() const now = new Date()

View File

@ -11,7 +11,7 @@ export class TaskFactory extends Factory {
id: '{increment}', id: '{increment}',
title: faker.lorem.words(3), title: faker.lorem.words(3),
done: false, done: false,
list_id: 1, project_id: 1,
created_by_id: 1, created_by_id: 1,
index: '{increment}', index: '{increment}',
position: '{increment}', position: '{increment}',

View File

@ -1,14 +1,14 @@
import {Factory} from '../support/factory' import {Factory} from '../support/factory'
export class UserListFactory extends Factory { export class UserProjectFactory extends Factory {
static table = 'users_lists' static table = 'users_projects'
static factory() { static factory() {
const now = new Date() const now = new Date()
return { return {
id: '{increment}', id: '{increment}',
list_id: 1, project_id: 1,
user_id: 1, user_id: 1,
right: 0, right: 0,
created: now.toISOString(), created: now.toISOString(),

View File

@ -30,21 +30,21 @@ A basic service can look like this:
```javascript ```javascript
import AbstractService from './abstractService' import AbstractService from './abstractService'
import ListModel from '../models/list' import ProjectModel from '../models/project'
export default class ListService extends AbstractService { export default class ProjectService extends AbstractService {
constructor() { constructor() {
super({ super({
getAll: '/lists', getAll: '/projects',
get: '/lists/{id}', get: '/projects/{id}',
create: '/namespaces/{namespaceID}/lists', create: '/namespaces/{namespaceID}/projects',
update: '/lists/{id}', update: '/projects/{id}',
delete: '/lists/{id}', delete: '/projects/{id}',
}) })
} }
modelFactory(data) { modelFactory(data) {
return new ListModel(data) return new ProjectModel(data)
} }
} }
``` ```
@ -132,7 +132,7 @@ import AbstractModel from './abstractModel'
import TaskModel from './task' import TaskModel from './task'
import UserModel from './user' import UserModel from './user'
export default class ListModel extends AbstractModel { export default class ProjectModel extends AbstractModel {
constructor(data) { constructor(data) {
// The constructor of AbstractModel handles all the default parsing. // The constructor of AbstractModel handles all the default parsing.

View File

@ -1,67 +1,50 @@
<template> <template>
<header <header :class="{ 'has-background': background, 'menu-active': menuActive }" aria-label="main navigation"
:class="{'has-background': background, 'menu-active': menuActive}" class="navbar d-print-none">
aria-label="main navigation" <router-link :to="{ name: 'home' }" class="logo-link">
class="navbar d-print-none" <Logo width="164" height="48" />
>
<router-link :to="{name: 'home'}" class="logo-link">
<Logo width="164" height="48"/>
</router-link> </router-link>
<MenuButton class="menu-button"/> <MenuButton class="menu-button" />
<div <div v-if="currentProject.id" class="project-title-wrapper">
v-if="currentList.id" <h1 class="project-title">{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
class="list-title-wrapper" </h1>
>
<h1 class="list-title">{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}</h1> <BaseButton :to="{ name: 'project.info', params: { projectId: currentProject.id } }" class="project-title-button">
<icon icon="circle-info" />
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="list-title-button">
<icon icon="circle-info"/>
</BaseButton> </BaseButton>
<list-settings-dropdown <project-settings-dropdown v-if="canWriteCurrentProject && currentProject.id !== -1"
v-if="canWriteCurrentList && currentList.id !== -1" class="project-title-dropdown" :project="currentProject">
class="list-title-dropdown" <template #trigger="{ toggleOpen }">
:list="currentList" <BaseButton class="project-title-button" @click="toggleOpen">
> <icon icon="ellipsis-h" class="icon" />
<template #trigger="{toggleOpen}">
<BaseButton class="list-title-button" @click="toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton> </BaseButton>
</template> </template>
</list-settings-dropdown> </project-settings-dropdown>
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<BaseButton <BaseButton @click="openQuickActions" class="trigger-button" v-shortcut="'Control+k'"
@click="openQuickActions" :title="$t('keyboardShortcuts.quickSearch')">
class="trigger-button" <icon icon="search" />
v-shortcut="'Control+k'"
:title="$t('keyboardShortcuts.quickSearch')"
>
<icon icon="search"/>
</BaseButton> </BaseButton>
<Notifications /> <Notifications />
<dropdown> <dropdown>
<template #trigger="{toggleOpen, open}"> <template #trigger="{ toggleOpen, open }">
<BaseButton <BaseButton class="username-dropdown-trigger" @click="toggleOpen" variant="secondary" :shadow="false">
class="username-dropdown-trigger" <img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40" />
@click="toggleOpen"
variant="secondary"
:shadow="false"
>
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
<span class="username">{{ authStore.userDisplayName }}</span> <span class="username">{{ authStore.userDisplayName }}</span>
<span class="icon is-small" :style="{ <span class="icon is-small" :style="{
transform: open ? 'rotate(180deg)' : 'rotate(0)', transform: open ? 'rotate(180deg)' : 'rotate(0)',
}"> }">
<icon icon="chevron-down"/> <icon icon="chevron-down" />
</span> </span>
</BaseButton> </BaseButton>
</template> </template>
<dropdown-item :to="{name: 'user.settings'}"> <dropdown-item :to="{ name: 'user.settings' }">
{{ $t('user.settings.title') }} {{ $t('user.settings.title') }}
</dropdown-item> </dropdown-item>
<dropdown-item v-if="imprintUrl" :href="imprintUrl"> <dropdown-item v-if="imprintUrl" :href="imprintUrl">
@ -73,7 +56,7 @@
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)"> <dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
{{ $t('keyboardShortcuts.title') }} {{ $t('keyboardShortcuts.title') }}
</dropdown-item> </dropdown-item>
<dropdown-item :to="{name: 'about'}"> <dropdown-item :to="{ name: 'about' }">
{{ $t('about.title') }} {{ $t('about.title') }}
</dropdown-item> </dropdown-item>
<dropdown-item @click="authStore.logout()"> <dropdown-item @click="authStore.logout()">
@ -85,11 +68,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed} from 'vue' import { computed } from 'vue'
import {RIGHTS as Rights} from '@/constants/rights' import { RIGHTS as Rights } from '@/constants/rights'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue' import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import Notifications from '@/components/notifications/notifications.vue' import Notifications from '@/components/notifications/notifications.vue'
@ -97,16 +80,16 @@ import Logo from '@/components/home/Logo.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import MenuButton from '@/components/home/MenuButton.vue' import MenuButton from '@/components/home/MenuButton.vue'
import {getListTitle} from '@/helpers/getListTitle' import { getProjectTitle } from '@/helpers/getProjectTitle'
import {useBaseStore} from '@/stores/base' import { useBaseStore } from '@/stores/base'
import {useConfigStore} from '@/stores/config' import { useConfigStore } from '@/stores/config'
import {useAuthStore} from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
const baseStore = useBaseStore() const baseStore = useBaseStore()
const currentList = computed(() => baseStore.currentList) const currentProject = computed(() => baseStore.currentProject)
const background = computed(() => baseStore.background) const background = computed(() => baseStore.background)
const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ) const canWriteCurrentProject = computed(() => baseStore.currentProject.maxRight > Rights.READ)
const menuActive = computed(() => baseStore.menuActive) const menuActive = computed(() => baseStore.menuActive)
const authStore = useAuthStore() const authStore = useAuthStore()
@ -166,7 +149,7 @@ $user-dropdown-width-mobile: 5rem;
.logo-link { .logo-link {
display: none; display: none;
@media screen and (min-width: $tablet) { @media screen and (min-width: $tablet) {
align-self: stretch; align-self: stretch;
display: flex; display: flex;
@ -185,12 +168,12 @@ $user-dropdown-width-mobile: 5rem;
} }
} }
.list-title-wrapper { .project-title-wrapper {
margin-inline: auto; margin-inline: auto;
display: flex; display: flex;
align-items: center; align-items: center;
// this makes the truncated text of the list title work // this makes the truncated text of the project title work
// inside the flexbox parent // inside the flexbox parent
min-width: 0; min-width: 0;
@ -199,7 +182,7 @@ $user-dropdown-width-mobile: 5rem;
} }
} }
.list-title { .project-title {
font-size: 1rem; font-size: 1rem;
// We need the following for overflowing ellipsis to work // We need the following for overflowing ellipsis to work
text-overflow: ellipsis; text-overflow: ellipsis;
@ -211,15 +194,15 @@ $user-dropdown-width-mobile: 5rem;
} }
} }
.list-title-dropdown { .project-title-dropdown {
align-self: stretch; align-self: stretch;
.list-title-button { .project-title-button {
flex-grow: 1; flex-grow: 1;
} }
} }
.list-title-button { .project-title-button {
align-self: stretch; align-self: stretch;
min-width: var(--navbar-button-min-width); min-width: var(--navbar-button-min-width);
display: flex; display: flex;
@ -235,7 +218,7 @@ $user-dropdown-width-mobile: 5rem;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
> * { >* {
min-width: var(--navbar-button-min-width); min-width: var(--navbar-button-min-width);
} }
} }

View File

@ -33,7 +33,7 @@
<quick-actions/> <quick-actions/>
<router-view :route="routeWithModal" v-slot="{ Component }"> <router-view :route="routeWithModal" v-slot="{ Component }">
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']"> <keep-alive :include="['project.list', 'project.gantt', 'project.table', 'project.kanban']">
<component :is="Component"/> <component :is="Component"/>
</keep-alive> </keep-alive>
</router-view> </router-view>
@ -87,7 +87,7 @@ function showKeyboardShortcuts() {
const route = useRoute() const route = useRoute()
// FIXME: this is really error prone // FIXME: this is really error prone
// Reset the current list highlight in menu if the current route is not list related. // Reset the current project highlight in menu if the current route is not project related.
watch(() => route.name as string, (routeName) => { watch(() => route.name as string, (routeName) => {
if ( if (
routeName && routeName &&
@ -106,7 +106,7 @@ watch(() => route.name as string, (routeName) => {
routeName.startsWith('user.settings') routeName.startsWith('user.settings')
) )
) { ) {
baseStore.handleSetCurrentList({list: null}) baseStore.handleSetCurrentProject({project: null})
} }
}) })

View File

@ -9,9 +9,9 @@
<Logo class="logo" v-if="logoVisible"/> <Logo class="logo" v-if="logoVisible"/>
<h1 <h1
:class="{'m-0': !logoVisible}" :class="{'m-0': !logoVisible}"
:style="{ 'opacity': currentList.title === '' ? '0': '1' }" :style="{ 'opacity': currentProject.title === '' ? '0': '1' }"
class="title"> class="title">
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }} {{ currentProject.title === '' ? $t('misc.loading') : currentProject.title }}
</h1> </h1>
<div class="box has-text-left view"> <div class="box has-text-left view">
<router-view/> <router-view/>
@ -31,7 +31,7 @@ import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue' import PoweredByLink from './PoweredByLink.vue'
const baseStore = useBaseStore() const baseStore = useBaseStore()
const currentList = computed(() => baseStore.currentList) 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)
</script> </script>

View File

@ -52,37 +52,37 @@
<template v-for="(n, nk) in namespaces" :key="n.id"> <template v-for="(n, nk) in namespaces" :key="n.id">
<div class="namespace-title" :class="{'has-menu': n.id > 0}"> <div class="namespace-title" :class="{'has-menu': n.id > 0}">
<BaseButton <BaseButton
@click="toggleLists(n.id)" @click="toggleProjects(n.id)"
class="menu-label" class="menu-label"
v-tooltip="namespaceTitles[nk]" v-tooltip="namespaceTitles[nk]"
> >
<ColorBubble <ColorBubble
v-if="n.hexColor !== ''" v-if="n.hexColor !== ''"
:color="n.hexColor" :color="n.hexColor"
class="mr-1" class="mr-1"
/> />
<span class="name">{{ namespaceTitles[nk] }}</span> <span class="name">{{ namespaceTitles[nk] }}</span>
<div <div
class="icon menu-item-icon is-small toggle-lists-icon pl-2" class="icon menu-item-icon is-small toggle-lists-icon pl-2"
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}" :class="{'active': typeof projectsVisible[n.id] !== 'undefined' ? projectsVisible[n.id] : true}"
> >
<icon icon="chevron-down"/> <icon icon="chevron-down"/>
</div> </div>
<span class="count" :class="{'ml-2 mr-0': n.id > 0}"> <span class="count" :class="{'ml-2 mr-0': n.id > 0}">
({{ namespaceListsCount[nk] }}) ({{ namespaceProjectsCount[nk] }})
</span> </span>
</BaseButton> </BaseButton>
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/> <namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
</div> </div>
<!-- <!--
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function NOTE: a v-model / computed setter is not possible, since the updateActiveProjects function
triggered by the change needs to have access to the current namespace triggered by the change needs to have access to the current namespace
--> -->
<draggable <draggable
v-if="listsVisible[n.id] ?? true" v-if="projectsVisible[n.id] ?? true"
v-bind="dragOptions" v-bind="dragOptions"
:modelValue="activeLists[nk]" :modelValue="activeProjects[nk]"
@update:modelValue="(lists) => updateActiveLists(n, lists)" @update:modelValue="(projects) => updateActiveProjects(n, projects)"
group="namespace-lists" group="namespace-lists"
@start="() => drag = true" @start="() => drag = true"
@end="saveListPosition" @end="saveListPosition"
@ -100,46 +100,46 @@
{ 'dragging-disabled': n.id < 0 } { 'dragging-disabled': n.id < 0 }
] ]
}" }"
> >
<template #item="{element: l}"> <template #item="{element: l}">
<li <li
class="list-menu loader-container is-loading-small" class="list-menu loader-container is-loading-small"
:class="{'is-loading': listUpdating[l.id]}" :class="{'is-loading': projectUpdating[l.id]}"
>
<BaseButton
:to="{ name: 'project.index', params: { projectId: l.id} }"
class="list-menu-link"
:class="{'router-link-exact-active': currentProject.id === l.id}"
> >
<BaseButton <span class="icon menu-item-icon handle">
:to="{ name: 'list.index', params: { listId: l.id} }" <icon icon="grip-lines"/>
class="list-menu-link" </span>
:class="{'router-link-exact-active': currentList.id === l.id}" <ColorBubble
>
<span class="icon menu-item-icon handle">
<icon icon="grip-lines"/>
</span>
<ColorBubble
v-if="l.hexColor !== ''" v-if="l.hexColor !== ''"
:color="l.hexColor" :color="l.hexColor"
class="mr-1" class="mr-1"
/> />
<span class="list-menu-title">{{ getListTitle(l) }}</span> <span class="list-menu-title">{{ getProjectTitle(l) }}</span>
</BaseButton> </BaseButton>
<BaseButton <BaseButton
v-if="l.id > 0" v-if="l.id > 0"
class="favorite" class="favorite"
:class="{'is-favorite': l.isFavorite}" :class="{'is-favorite': l.isFavorite}"
@click="listStore.toggleListFavorite(l)" @click="projectStore.toggleProjectFavorite(l)"
> >
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/> <icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton> </BaseButton>
<list-settings-dropdown class="menu-list-dropdown" :list="l" v-if="l.id > 0"> <ProjectSettingsDropdown class="menu-list-dropdown" :project="l" v-if="l.id > 0">
<template #trigger="{toggleOpen}"> <template #trigger="{toggleOpen}">
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen"> <BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
<icon icon="ellipsis-h" class="icon"/> <icon icon="ellipsis-h" class="icon"/>
</BaseButton> </BaseButton>
</template> </template>
</list-settings-dropdown> </ProjectSettingsDropdown>
<span class="list-setting-spacer" v-else></span> <span class="list-setting-spacer" v-else></span>
</li> </li>
</template> </template>
</draggable> </draggable>
</template> </template>
</nav> </nav>
<PoweredByLink/> <PoweredByLink/>
@ -152,20 +152,20 @@ import draggable from 'zhyswan-vuedraggable'
import type {SortableEvent} from 'sortablejs' import type {SortableEvent} from 'sortablejs'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue' import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue' import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
import PoweredByLink from '@/components/home/PoweredByLink.vue' import PoweredByLink from '@/components/home/PoweredByLink.vue'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle' import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getListTitle} from '@/helpers/getListTitle' import {getProjectTitle} from '@/helpers/getProjectTitle'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
const drag = ref(false) const drag = ref(false)
@ -176,7 +176,7 @@ const dragOptions = {
const baseStore = useBaseStore() const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const currentList = computed(() => baseStore.currentList) const currentProject = computed(() => baseStore.currentProject)
const menuActive = computed(() => baseStore.menuActive) const menuActive = computed(() => baseStore.menuActive)
const loading = computed(() => namespaceStore.isLoading) const loading = computed(() => namespaceStore.isLoading)
@ -184,9 +184,9 @@ const loading = computed(() => namespaceStore.isLoading)
const namespaces = computed(() => { const namespaces = computed(() => {
return namespaceStore.namespaces.filter(n => !n.isArchived) return namespaceStore.namespaces.filter(n => !n.isArchived)
}) })
const activeLists = computed(() => { const activeProjects = computed(() => {
return namespaces.value.map(({lists}) => { return namespaces.value.map(({projects}) => {
return lists?.filter(item => { return projects?.filter(item => {
return typeof item !== 'undefined' && !item.isArchived return typeof item !== 'undefined' && !item.isArchived
}) })
}) })
@ -196,45 +196,45 @@ const namespaceTitles = computed(() => {
return namespaces.value.map((namespace) => getNamespaceTitle(namespace)) return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
}) })
const namespaceListsCount = computed(() => { const namespaceProjectsCount = computed(() => {
return namespaces.value.map((_, index) => activeLists.value[index]?.length ?? 0) return namespaces.value.map((_, index) => activeProjects.value[index]?.length ?? 0)
}) })
const listStore = useListStore() const projectStore = useProjectStore()
function toggleLists(namespaceId: INamespace['id']) { function toggleProjects(namespaceId: INamespace['id']) {
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId] projectsVisible.value[namespaceId] = !projectsVisible.value[namespaceId]
} }
const listsVisible = ref<{ [id: INamespace['id']]: boolean }>({}) const projectsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
// FIXME: async action will be unfinished when component mounts // FIXME: async action will be unfinished when component mounts
onBeforeMount(async () => { onBeforeMount(async () => {
const namespaces = await namespaceStore.loadNamespaces() const namespaces = await namespaceStore.loadNamespaces()
namespaces.forEach(n => { namespaces.forEach(n => {
if (typeof listsVisible.value[n.id] === 'undefined') { if (typeof projectsVisible.value[n.id] === 'undefined') {
listsVisible.value[n.id] = true projectsVisible.value[n.id] = true
} }
}) })
}) })
function updateActiveLists(namespace: INamespace, activeLists: IList[]) { function updateActiveProjects(namespace: INamespace, activeProjects: IProject[]) {
// This is a bit hacky: since we do have to filter out the archived items from the list // This is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it. // for vue draggable updating it is not as simple as replacing it.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order // To work around this, we merge the active projects with the archived ones. Doing so breaks the order
// because now all archived lists are sorted after the active ones. This is fine because they are sorted // because now all archived projects are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier. // later when showing them anyway, and it makes the merging happening here a lot easier.
const lists = [ const projects = [
...activeLists, ...activeProjects,
...namespace.lists.filter(l => l.isArchived), ...namespace.projects.filter(l => l.isArchived),
] ]
namespaceStore.setNamespaceById({ namespaceStore.setNamespaceById({
...namespace, ...namespace,
lists, projects,
}) })
} }
const listUpdating = ref<{ [id: INamespace['id']]: boolean }>({}) const projectUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
async function saveListPosition(e: SortableEvent) { async function saveListPosition(e: SortableEvent) {
if (!e.newIndex && e.newIndex !== 0) return if (!e.newIndex && e.newIndex !== 0) return
@ -242,31 +242,31 @@ async function saveListPosition(e: SortableEvent) {
const namespaceId = parseInt(e.to.dataset.namespaceId as string) const namespaceId = parseInt(e.to.dataset.namespaceId as string)
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string) const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
const listsActive = activeLists.value[newNamespaceIndex] const projectsActive = activeProjects.value[newNamespaceIndex]
// If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive // If the project was dragged to the last position, Safari will report e.newIndex as the size of the projectsActive
// array instead of using the position. Because the index is wrong in that case, dragging the list will fail. // array instead of using the position. Because the index is wrong in that case, dragging the project will fail.
// To work around that we're explicitly checking that case here and decrease the index. // To work around that we're explicitly checking that case here and decrease the index.
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex const newIndex = e.newIndex === projectsActive.length ? e.newIndex - 1 : e.newIndex
const list = listsActive[newIndex] const project = projectsActive[newIndex]
const listBefore = listsActive[newIndex - 1] ?? null const projectBefore = projectsActive[newIndex - 1] ?? null
const listAfter = listsActive[newIndex + 1] ?? null const projectAfter = projectsActive[newIndex + 1] ?? null
listUpdating.value[list.id] = true projectUpdating.value[project.id] = true
const position = calculateItemPosition( const position = calculateItemPosition(
listBefore !== null ? listBefore.position : null, projectBefore !== null ? projectBefore.position : null,
listAfter !== null ? listAfter.position : null, projectAfter !== null ? projectAfter.position : null,
) )
try { try {
// create a copy of the list in order to not violate pinia manipulation // create a copy of the project in order to not violate pinia manipulation
await listStore.updateList({ await projectStore.updateProject({
...list, ...project,
position, position,
namespaceId, namespaceId,
}) })
} finally { } finally {
listUpdating.value[list.id] = false projectUpdating.value[project.id] = false
} }
} }
</script> </script>

View File

@ -1,12 +1,12 @@
<template> <template>
<multiselect <multiselect
v-model="selectedLists" v-model="selectedProjects"
:search-results="foundLists" :search-results="foundProjects"
:loading="listService.loading" :loading="projectService.loading"
:multiple="true" :multiple="true"
:placeholder="$t('list.search')" :placeholder="$t('project.search')"
label="title" label="title"
@search="findLists" @search="findProjects"
/> />
</template> </template>
@ -15,49 +15,49 @@ import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import ListService from '@/services/list' import ProjectService from '@/services/project'
import {includesById} from '@/helpers/utils' import {includesById} from '@/helpers/utils'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Array as PropType<IList[]>, type: Array as PropType<IProject[]>,
default: () => [], default: () => [],
}, },
}) })
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: IList[]): void (e: 'update:modelValue', value: IProject[]): void
}>() }>()
const lists = ref<IList[]>([]) const projects = ref<IProject[]>([])
watchEffect(() => { watchEffect(() => {
lists.value = props.modelValue projects.value = props.modelValue
}) })
const selectedLists = computed({ const selectedProjects = computed({
get() { get() {
return lists.value return projects.value
}, },
set: (value) => { set: (value) => {
lists.value = value projects.value = value
emit('update:modelValue', value) emit('update:modelValue', value)
}, },
}) })
const listService = shallowReactive(new ListService()) const projectService = shallowReactive(new ProjectService())
const foundLists = ref<IList[]>([]) const foundProjects = ref<IProject[]>([])
async function findLists(query: string) { async function findProjects(query: string) {
if (query === '') { if (query === '') {
foundLists.value = [] foundProjects.value = []
return return
} }
const response = await listService.getAll({}, {s: query}) as IList[] const response = await projectService.getAll({}, {s: query}) as IProject[]
// Filter selected items from the results // Filter selected items from the results
foundLists.value = response.filter(({id}) => !includesById(lists.value, id)) foundProjects.value = response.filter(({id}) => !includesById(projects.value, id))
} }
</script> </script>

View File

@ -286,11 +286,11 @@ function handleCheckboxClick(e: Event) {
console.debug('no index found') console.debug('no index found')
return return
} }
const listPrefix = text.value.substring(index, index + 1) const projectPrefix = text.value.substring(index, index + 1)
console.debug({index, listPrefix, checked, text: text.value}) console.debug({index, projectPrefix, checked, text: text.value})
text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `) text.value = replaceAt(text.value, index, `${projectPrefix} ${checked ? '[x]' : '[ ]'} `)
bubble() bubble()
renderPreview() renderPreview()
} }

View File

@ -61,8 +61,8 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
], ],
}, },
{ {
title: 'list.kanban.title', title: 'project.kanban.title',
available: (route) => route.name === 'list.kanban', available: (route) => route.name === 'project.kanban',
shortcuts: [ shortcuts: [
{ {
title: 'keyboardShortcuts.task.done', title: 'keyboardShortcuts.task.done',
@ -71,26 +71,26 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
], ],
}, },
{ {
title: 'keyboardShortcuts.list.title', title: 'keyboardShortcuts.project.title',
available: (route) => (route.name as string)?.startsWith('list.'), available: (route) => (route.name as string)?.startsWith('project.'),
shortcuts: [ shortcuts: [
{ {
title: 'keyboardShortcuts.list.switchToListView', title: 'keyboardShortcuts.project.switchToProjectView',
keys: ['g', 'l'], keys: ['g', 'l'],
combination: 'then', combination: 'then',
}, },
{ {
title: 'keyboardShortcuts.list.switchToGanttView', title: 'keyboardShortcuts.project.switchToGanttView',
keys: ['g', 'g'], keys: ['g', 'g'],
combination: 'then', combination: 'then',
}, },
{ {
title: 'keyboardShortcuts.list.switchToTableView', title: 'keyboardShortcuts.project.switchToTableView',
keys: ['g', 't'], keys: ['g', 't'],
combination: 'then', combination: 'then',
}, },
{ {
title: 'keyboardShortcuts.list.switchToKanbanView', title: 'keyboardShortcuts.project.switchToKanbanView',
keys: ['g', 'k'], keys: ['g', 'k'],
combination: 'then', combination: 'then',
}, },

View File

@ -73,14 +73,14 @@ const {t} = useI18n({useScope: 'global'})
const tooltipText = computed(() => { const tooltipText = computed(() => {
if (disabled.value) { if (disabled.value) {
if (props.entity === 'list' && subscriptionEntity.value === 'namespace') { if (props.entity === 'project' && subscriptionEntity.value === 'namespace') {
return t('task.subscription.subscribedListThroughParentNamespace') return t('task.subscription.subscribedProjectThroughParentNamespace')
} }
if (props.entity === 'task' && subscriptionEntity.value === 'namespace') { if (props.entity === 'task' && subscriptionEntity.value === 'namespace') {
return t('task.subscription.subscribedTaskThroughParentNamespace') return t('task.subscription.subscribedTaskThroughParentNamespace')
} }
if (props.entity === 'task' && subscriptionEntity.value === 'list') { if (props.entity === 'task' && subscriptionEntity.value === 'project') {
return t('task.subscription.subscribedTaskThroughParentList') return t('task.subscription.subscribedTaskThroughParentProject')
} }
return '' return ''
@ -91,10 +91,10 @@ const tooltipText = computed(() => {
return props.modelValue !== null ? return props.modelValue !== null ?
t('task.subscription.subscribedNamespace') : t('task.subscription.subscribedNamespace') :
t('task.subscription.notSubscribedNamespace') t('task.subscription.notSubscribedNamespace')
case 'list': case 'project':
return props.modelValue !== null ? return props.modelValue !== null ?
t('task.subscription.subscribedList') : t('task.subscription.subscribedProject') :
t('task.subscription.notSubscribedList') t('task.subscription.notSubscribedProject')
case 'task': case 'task':
return props.modelValue !== null ? return props.modelValue !== null ?
t('task.subscription.subscribedTask') : t('task.subscription.subscribedTask') :
@ -133,8 +133,8 @@ async function subscribe() {
case 'namespace': case 'namespace':
message = t('task.subscription.subscribeSuccessNamespace') message = t('task.subscription.subscribeSuccessNamespace')
break break
case 'list': case 'project':
message = t('task.subscription.subscribeSuccessList') message = t('task.subscription.subscribeSuccessProject')
break break
case 'task': case 'task':
message = t('task.subscription.subscribeSuccessTask') message = t('task.subscription.subscribeSuccessTask')
@ -156,8 +156,8 @@ async function unsubscribe() {
case 'namespace': case 'namespace':
message = t('task.subscription.unsubscribeSuccessNamespace') message = t('task.subscription.unsubscribeSuccessNamespace')
break break
case 'list': case 'project':
message = t('task.subscription.unsubscribeSuccessList') message = t('task.subscription.unsubscribeSuccessProject')
break break
case 'task': case 'task':
message = t('task.subscription.unsubscribeSuccessTask') message = t('task.subscription.unsubscribeSuccessTask')

View File

@ -30,10 +30,10 @@
{{ $t('menu.share') }} {{ $t('menu.share') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'list.create', params: { namespaceId: namespace.id } }" :to="{ name: 'project.create', params: { namespaceId: namespace.id } }"
icon="plus" icon="plus"
> >
{{ $t('menu.newList') }} {{ $t('menu.newProject') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }" :to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"

View File

@ -117,9 +117,9 @@ function to(n, index) {
case names.TASK_DELETED: case names.TASK_DELETED:
// Nothing // Nothing
break break
case names.LIST_CREATED: case names.PROJECT_CREATED:
to.name = 'task.index' to.name = 'task.index'
to.params.listId = n.notification.list.id to.params.projectId = n.notification.project.id
break break
case names.TEAM_MEMBER_ADDED: case names.TEAM_MEMBER_ADDED:
to.name = 'teams.edit' to.name = 'teams.edit'

View File

@ -1,56 +1,56 @@
<template> <template>
<div <div
:class="{ 'is-loading': listService.loading, 'is-archived': currentList.isArchived}" :class="{ 'is-loading': projectService.loading, 'is-archived': currentProject.isArchived}"
class="loader-container" class="loader-container"
> >
<div class="switch-view-container"> <div class="switch-view-container">
<div class="switch-view"> <div class="switch-view">
<BaseButton <BaseButton
v-shortcut="'g l'" v-shortcut="'g l'"
:title="$t('keyboardShortcuts.list.switchToListView')" :title="$t('keyboardShortcuts.project.switchToProjectView')"
class="switch-view-button" class="switch-view-button"
:class="{'is-active': viewName === 'list'}" :class="{'is-active': viewName === 'project'}"
:to="{ name: 'list.list', params: { listId } }" :to="{ name: 'project.list', params: { projectId } }"
> >
{{ $t('list.list.title') }} {{ $t('project.list.title') }}
</BaseButton> </BaseButton>
<BaseButton <BaseButton
v-shortcut="'g g'" v-shortcut="'g g'"
:title="$t('keyboardShortcuts.list.switchToGanttView')" :title="$t('keyboardShortcuts.project.switchToGanttView')"
class="switch-view-button" class="switch-view-button"
:class="{'is-active': viewName === 'gantt'}" :class="{'is-active': viewName === 'gantt'}"
:to="{ name: 'list.gantt', params: { listId } }" :to="{ name: 'project.gantt', params: { projectId } }"
> >
{{ $t('list.gantt.title') }} {{ $t('project.gantt.title') }}
</BaseButton> </BaseButton>
<BaseButton <BaseButton
v-shortcut="'g t'" v-shortcut="'g t'"
:title="$t('keyboardShortcuts.list.switchToTableView')" :title="$t('keyboardShortcuts.project.switchToTableView')"
class="switch-view-button" class="switch-view-button"
:class="{'is-active': viewName === 'table'}" :class="{'is-active': viewName === 'table'}"
:to="{ name: 'list.table', params: { listId } }" :to="{ name: 'project.table', params: { projectId } }"
> >
{{ $t('list.table.title') }} {{ $t('project.table.title') }}
</BaseButton> </BaseButton>
<BaseButton <BaseButton
v-shortcut="'g k'" v-shortcut="'g k'"
:title="$t('keyboardShortcuts.list.switchToKanbanView')" :title="$t('keyboardShortcuts.project.switchToKanbanView')"
class="switch-view-button" class="switch-view-button"
:class="{'is-active': viewName === 'kanban'}" :class="{'is-active': viewName === 'kanban'}"
:to="{ name: 'list.kanban', params: { listId } }" :to="{ name: 'project.kanban', params: { projectId } }"
> >
{{ $t('list.kanban.title') }} {{ $t('project.kanban.title') }}
</BaseButton> </BaseButton>
</div> </div>
<slot name="header" /> <slot name="header" />
</div> </div>
<CustomTransition name="fade"> <transition name="fade">
<Message variant="warning" v-if="currentList.isArchived" class="mb-4"> <Message variant="warning" v-if="currentProject.isArchived" class="mb-4">
{{ $t('list.archived') }} {{ $t('project.archived') }}
</Message> </Message>
</CustomTransition> </transition>
<slot v-if="loadedListId"/> <slot v-if="loadedProjectId"/>
</div> </div>
</template> </template>
@ -60,20 +60,19 @@ import {useRoute} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import ListService from '@/services/list' import ProjectService from '@/services/project'
import {getListTitle} from '@/helpers/getListTitle' import {getProjectTitle} from '@/helpers/getProjectTitle'
import {saveListToHistory} from '@/modules/listHistory' import {saveProjectToHistory} from '@/modules/projectHistory'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useProjectStore} from '@/stores/projects'
const props = defineProps({ const props = defineProps({
listId: { projectId: {
type: Number, type: Number,
required: true, required: true,
}, },
@ -86,64 +85,64 @@ const props = defineProps({
const route = useRoute() const route = useRoute()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const listStore = useListStore() const projectStore = useProjectStore()
const listService = ref(new ListService()) const projectService = ref(new ProjectService())
const loadedListId = ref(0) const loadedProjectId = ref(0)
const currentList = computed(() => { const currentProject = computed(() => {
return typeof baseStore.currentList === 'undefined' ? { return typeof baseStore.currentProject === 'undefined' ? {
id: 0, id: 0,
title: '', title: '',
isArchived: false, isArchived: false,
maxRight: null, maxRight: null,
} : baseStore.currentList } : baseStore.currentProject
}) })
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '') useTitle(() => currentProject.value.id ? getProjectTitle(currentProject.value) : '')
// 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 list 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 list 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
// list background and then navigating to home. It also highlighted the list 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.listId, () => props.projectId,
// loadList // loadProject
async (listIdToLoad: number) => { async (projectIdToLoad: number) => {
const listData = {id: listIdToLoad} const projectData = {id: projectIdToLoad}
saveListToHistory(listData) saveProjectToHistory(projectData)
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and // Don't load the project if we either already loaded it or aren't dealing with a project at all currently and
// the currently loaded list has the right set. // the currently loaded project has the right set.
if ( if (
( (
listIdToLoad === loadedListId.value || projectIdToLoad === loadedProjectId.value ||
typeof listIdToLoad === 'undefined' || typeof projectIdToLoad === 'undefined' ||
listIdToLoad === currentList.value.id projectIdToLoad === currentProject.value.id
) )
&& typeof currentList.value !== 'undefined' && currentList.value.maxRight !== null && typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null
) { ) {
loadedListId.value = props.listId loadedProjectId.value = props.projectId
return return
} }
console.debug(`Loading list, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedListId = ${loadedListId.value}, currentList = `, currentList.value) console.debug(`Loading project, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedProjectId = ${loadedProjectId.value}, currentProject = `, currentProject.value)
// Set the current list 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
loadedListId.value = 0 loadedProjectId.value = 0
const listFromStore = listStore.getListById(listData.id) const projectFromStore = projectStore.getProjectById(projectData.id)
if (listFromStore !== null) { if (projectFromStore !== null) {
baseStore.setBackground(null) baseStore.setBackground(null)
baseStore.setBlurHash(null) baseStore.setBlurHash(null)
baseStore.handleSetCurrentList({list: listFromStore}) baseStore.handleSetCurrentProject({project: projectFromStore})
} }
// We create an extra list object instead of creating it in list.value because that would trigger a ui update which would result in bad ux. // We create an extra project object instead of creating it in project.value because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData) const project = new ProjectModel(projectData)
try { try {
const loadedList = await listService.value.get(list) const loadedProject = await projectService.value.get(project)
baseStore.handleSetCurrentList({list: loadedList}) baseStore.handleSetCurrentProject({project: loadedProject})
} finally { } finally {
loadedListId.value = props.listId loadedProjectId.value = props.projectId
} }
}, },
{immediate: true}, {immediate: true},

View File

@ -1,39 +1,39 @@
<template> <template>
<div <div
class="list-card" class="project-card"
:class="{ :class="{
'has-light-text': background !== null, 'has-light-text': background !== null,
'has-background': blurHashUrl !== '' || background !== null 'has-background': blurHashUrl !== '' || background !== null
}" }"
:style="{ :style="{
'border-left': list.hexColor ? `0.25rem solid ${list.hexColor}` : undefined, 'border-left': project.hexColor ? `0.25rem solid ${project.hexColor}` : undefined,
'background-image': blurHashUrl !== '' ? `url(${blurHashUrl})` : undefined, 'background-image': blurHashUrl !== '' ? `url(${blurHashUrl})` : undefined,
}" }"
> >
<div <div
class="list-background background-fade-in" class="project-background background-fade-in"
:class="{'is-visible': background}" :class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : undefined}" :style="{'background-image': background !== null ? `url(${background})` : undefined}"
/> />
<span v-if="list.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span> <span v-if="project.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
<div class="list-title" aria-hidden="true">{{ list.title }}</div> <div class="project-title" aria-hidden="true">{{ project.title }}</div>
<BaseButton <BaseButton
class="list-button" class="project-button"
:aria-label="list.title" :aria-label="project.title"
:title="list.description" :title="project.description"
:to="{ :to="{
name: 'list.index', name: 'project.index',
params: { listId: list.id} params: { projectId: project.id}
}" }"
/> />
<BaseButton <BaseButton
v-if="!list.isArchived" v-if="!project.isArchived"
class="favorite" class="favorite"
:class="{'is-favorite': list.isFavorite}" :class="{'is-favorite': project.isFavorite}"
@click.prevent.stop="listStore.toggleListFavorite(list)" @click.prevent.stop="projectStore.toggleProjectFavorite(project)"
> >
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" /> <icon :icon="project.isFavorite ? 'star' : ['far', 'star']" />
</BaseButton> </BaseButton>
</div> </div>
</template> </template>
@ -41,30 +41,30 @@
<script lang="ts" setup> <script lang="ts" setup>
import {toRef, type PropType} from 'vue' import {toRef, type PropType} from 'vue'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {useListBackground} from './useListBackground' import {useProjectBackground} from './useProjectBackground'
import {useListStore} from '@/stores/lists' import {useProjectStore} from '@/stores/projects'
const props = defineProps({ const props = defineProps({
list: { project: {
type: Object as PropType<IList>, type: Object as PropType<IProject>,
required: true, required: true,
}, },
}) })
const {background, blurHashUrl} = useListBackground(toRef(props, 'list')) const {background, blurHashUrl} = useProjectBackground(toRef(props, 'project'))
const listStore = useListStore() const projectStore = useProjectStore()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.list-card { .project-card {
--list-card-padding: 1rem; --project-card-padding: 1rem;
background: var(--white); background: var(--white);
padding: var(--list-card-padding); padding: var(--project-card-padding);
border-radius: $radius; border-radius: $radius;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
transition: box-shadow $transition; transition: box-shadow $transition;
@ -91,14 +91,14 @@ const listStore = useListStore()
} }
.has-background, .has-background,
.list-background { .project-background {
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
} }
.list-background, .project-background,
.list-button { .project-button {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
@ -111,7 +111,7 @@ const listStore = useListStore()
float: left; float: left;
} }
.list-title { .project-title {
align-self: flex-end; align-self: flex-end;
font-family: $vikunja-font; font-family: $vikunja-font;
font-weight: 400; font-weight: 400;
@ -120,7 +120,7 @@ const listStore = useListStore()
color: var(--text); color: var(--text);
width: 100%; width: 100%;
margin-bottom: 0; margin-bottom: 0;
max-height: calc(100% - (var(--list-card-padding) + 1rem)); // padding & height of the "is archived" badge max-height: calc(100% - (var(--project-card-padding) + 1rem)); // padding & height of the "is archived" badge
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-word; word-break: break-word;
@ -130,11 +130,11 @@ const listStore = useListStore()
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.has-light-text .list-title { .has-light-text .project-title {
color: var(--grey-100); color: var(--grey-100);
} }
.has-background .list-title { .has-background .project-title {
text-shadow: text-shadow:
0 0 10px var(--black), 0 0 10px var(--black),
1px 1px 5px var(--grey-700), 1px 1px 5px var(--grey-700),
@ -144,8 +144,8 @@ const listStore = useListStore()
.favorite { .favorite {
position: absolute; position: absolute;
top: var(--list-card-padding); top: var(--project-card-padding);
right: var(--list-card-padding); right: var(--project-card-padding);
transition: opacity $transition, color $transition; transition: opacity $transition, color $transition;
opacity: 1; opacity: 1;
@ -165,7 +165,7 @@ const listStore = useListStore()
opacity: 0; opacity: 0;
} }
.list-card:hover .favorite { .project-card:hover .favorite {
opacity: 1; opacity: 1;
} }
} }

View File

@ -1,24 +1,24 @@
<template> <template>
<ul class="list-grid"> <ul class="project-grid">
<li <li
v-for="(item, index) in filteredLists" v-for="(item, index) in filteredProjects"
:key="`list_${item.id}_${index}`" :key="`project_${item.id}_${index}`"
class="list-grid-item" class="project-grid-item"
> >
<ListCard :list="item" /> <ProjectCard :project="item" />
</li> </li>
</ul> </ul>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, type PropType} from 'vue' import {computed, type PropType} from 'vue'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import ListCard from './ListCard.vue' import ProjectCard from './ProjectCard.vue'
const props = defineProps({ const props = defineProps({
lists: { projects: {
type: Array as PropType<IList[]>, type: Array as PropType<IProject[]>,
default: () => [], default: () => [],
}, },
showArchived: { showArchived: {
@ -31,46 +31,46 @@ const props = defineProps({
}, },
}) })
const filteredLists = computed(() => { const filteredProjects = computed(() => {
return props.showArchived return props.showArchived
? props.lists ? props.projects
: props.lists.filter(l => !l.isArchived) : props.projects.filter(l => !l.isArchived)
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$list-height: 150px; $project-height: 150px;
$list-spacing: 1rem; $project-spacing: 1rem;
.list-grid { .project-grid {
margin: 0; // reset li margin: 0; // reset li
list-style-type: none; project-style-type: none;
display: grid; display: grid;
grid-template-columns: repeat(var(--list-columns), 1fr); grid-template-columns: repeat(var(--project-columns), 1fr);
grid-auto-rows: $list-height; grid-auto-rows: $project-height;
gap: $list-spacing; gap: $project-spacing;
@media screen and (min-width: $mobile) { @media screen and (min-width: $mobile) {
--list-rows: 4; --project-rows: 4;
--list-columns: 1; --project-columns: 1;
} }
@media screen and (min-width: $mobile) and (max-width: $tablet) { @media screen and (min-width: $mobile) and (max-width: $tablet) {
--list-columns: 2; --project-columns: 2;
} }
@media screen and (min-width: $tablet) and (max-width: $widescreen) { @media screen and (min-width: $tablet) and (max-width: $widescreen) {
--list-columns: 3; --project-columns: 3;
--list-rows: 3; --project-rows: 3;
} }
@media screen and (min-width: $widescreen) { @media screen and (min-width: $widescreen) {
--list-columns: 5; --project-columns: 5;
--list-rows: 2; --project-rows: 2;
} }
} }
.list-grid-item { .project-grid-item {
display: grid; display: grid;
margin-top: 0; // remove padding coming form .content li + li margin-top: 0; // remove padding coming form .content li + li
} }

View File

@ -32,7 +32,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref, watch} from 'vue' import {computed, ref, watch} from 'vue'
import Filters from '@/components/list/partials/filters.vue' import Filters from '@/components/project/partials/filters.vue'
import {getDefaultParams} from '@/composables/useTaskList' import {getDefaultParams} from '@/composables/useTaskList'

View File

@ -20,7 +20,7 @@
{{ $t('filters.attributes.showDoneTasks') }} {{ $t('filters.attributes.showDoneTasks') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox <fancycheckbox
v-if="!['list.kanban', 'list.table'].includes($route.name as string)" v-if="!['project.kanban', 'project.table'].includes($route.name as string)"
v-model="sortAlphabetically" v-model="sortAlphabetically"
@update:model-value="change()" @update:model-value="change()"
> >
@ -154,14 +154,14 @@
</div> </div>
<template <template
v-if="['filters.create', 'list.edit', 'filter.settings.edit'].includes($route.name as string)"> v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)">
<div class="field"> <div class="field">
<label class="label">{{ $t('list.lists') }}</label> <label class="label">{{ $t('project.lists') }}</label>
<div class="control"> <div class="control">
<SelectList <SelectProject
v-model="entities.lists" v-model="entities.projects"
@select="changeMultiselectFilter('lists', 'list_id')" @select="changeMultiselectFilter('projects', 'project_id')"
@remove="changeMultiselectFilter('lists', 'list_id')" @remove="changeMultiselectFilter('projects', 'project_id')"
/> />
</div> </div>
</div> </div>
@ -190,7 +190,7 @@ import {camelCase} from 'camel-case'
import type {ILabel} from '@/modelTypes/ILabel' import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
@ -200,7 +200,7 @@ import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue
import EditLabels from '@/components/tasks/partials/editLabels.vue' import EditLabels from '@/components/tasks/partials/editLabels.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue' import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import SelectUser from '@/components/input/SelectUser.vue' import SelectUser from '@/components/input/SelectUser.vue'
import SelectList from '@/components/input/SelectList.vue' import SelectProject from '@/components/input/SelectProject.vue'
import SelectNamespace from '@/components/input/SelectNamespace.vue' import SelectNamespace from '@/components/input/SelectNamespace.vue'
import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {parseDateOrString} from '@/helpers/time/parseDateOrString'
@ -208,13 +208,13 @@ import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import UserService from '@/services/user' import UserService from '@/services/user'
import ListService from '@/services/list' import ProjectService from '@/services/project'
import NamespaceService from '@/services/namespace' import NamespaceService from '@/services/namespace'
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS // FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
import {getDefaultParams} from '@/composables/useTaskList' import {getDefaultParams} from '@/composables/useTaskList'
// FIXME: merge with DEFAULT_PARAMS in taskList.js // FIXME: merge with DEFAULT_PARAMS in taskProject.js
const DEFAULT_PARAMS = { const DEFAULT_PARAMS = {
sort_by: [], sort_by: [],
order_by: [], order_by: [],
@ -239,7 +239,7 @@ const DEFAULT_FILTERS = {
reminders: '', reminders: '',
assignees: '', assignees: '',
labels: '', labels: '',
list_id: '', project_id: '',
namespace: '', namespace: '',
} as const } as const
@ -264,23 +264,23 @@ const filters = ref({...DEFAULT_FILTERS})
const services = { const services = {
users: shallowReactive(new UserService()), users: shallowReactive(new UserService()),
lists: shallowReactive(new ListService()), projects: shallowReactive(new ProjectService()),
namespace: shallowReactive(new NamespaceService()), namespace: shallowReactive(new NamespaceService()),
} }
interface Entities { interface Entities {
users: IUser[] users: IUser[]
labels: ILabel[] labels: ILabel[]
lists: IList[] projects: IProject[]
namespace: INamespace[] namespace: INamespace[]
} }
type EntityType = 'users' | 'labels' | 'lists' | 'namespace' type EntityType = 'users' | 'labels' | 'projects' | 'namespace'
const entities: Entities = reactive({ const entities: Entities = reactive({
users: [], users: [],
labels: [], labels: [],
lists: [], projects: [],
namespace: [], namespace: [],
}) })
@ -327,7 +327,7 @@ function prepareFilters() {
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true) prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
prepareDate('reminders') prepareDate('reminders')
prepareRelatedObjectFilter('users', 'assignees') prepareRelatedObjectFilter('users', 'assignees')
prepareRelatedObjectFilter('lists', 'list_id') prepareRelatedObjectFilter('projects', 'project_id')
prepareRelatedObjectFilter('namespace') prepareRelatedObjectFilter('namespace')
prepareSingleValue('labels') prepareSingleValue('labels')

View File

@ -1,30 +1,30 @@
import {ref, watch, type Ref} from 'vue' import {ref, watch, type Ref} from 'vue'
import ListService from '@/services/list' import ProjectService from '@/services/project'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
export function useListBackground(list: Ref<IList>) { export function useProjectBackground(project: Ref<IProject>) {
const background = ref<string | null>(null) const background = ref<string | null>(null)
const backgroundLoading = ref(false) const backgroundLoading = ref(false)
const blurHashUrl = ref('') const blurHashUrl = ref('')
watch( watch(
() => [list.value.id, list.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']], () => [project.value.id, project.value.backgroundBlurHash] as [IProject['id'], IProject['backgroundBlurHash']],
async ([listId, blurHash], oldValue) => { async ([projectId, blurHash], oldValue) => {
if ( if (
list.value === null || project.value === null ||
!list.value.backgroundInformation || !project.value.backgroundInformation ||
backgroundLoading.value backgroundLoading.value
) { ) {
return return
} }
const [oldListId, oldBlurHash] = oldValue || [] const [oldProjectId, oldBlurHash] = oldValue || []
if ( if (
oldValue !== undefined && oldValue !== undefined &&
listId === oldListId && blurHash === oldBlurHash projectId === oldProjectId && blurHash === oldBlurHash
) { ) {
// list hasn't changed // project hasn't changed
return return
} }
@ -35,8 +35,8 @@ export function useListBackground(list: Ref<IList>) {
blurHashUrl.value = blurHash ? window.URL.createObjectURL(blurHash) : '' blurHashUrl.value = blurHash ? window.URL.createObjectURL(blurHash) : ''
}) })
const listService = new ListService() const projectService = new ProjectService()
const backgroundPromise = listService.background(list.value).then((result) => { const backgroundPromise = projectService.background(project.value).then((result) => {
background.value = result background.value = result
}) })
await Promise.all([blurHashPromise, backgroundPromise]) await Promise.all([blurHashPromise, backgroundPromise])
@ -44,7 +44,7 @@ export function useListBackground(list: Ref<IList>) {
backgroundLoading.value = false backgroundLoading.value = false
} }
}, },
{ immediate: true }, {immediate: true},
) )
return { return {
@ -52,4 +52,4 @@ export function useListBackground(list: Ref<IList>) {
blurHashUrl, blurHashUrl,
backgroundLoading, backgroundLoading,
} }
} }

View File

@ -8,24 +8,24 @@
</slot> </slot>
</template> </template>
<template v-if="isSavedFilter(list)"> <template v-if="isSavedFilter(project)">
<dropdown-item <dropdown-item
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }" :to="{ name: 'filter.settings.edit', params: { projectId: project.id } }"
icon="pen" icon="pen"
> >
{{ $t('menu.edit') }} {{ $t('menu.edit') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'filter.settings.delete', params: { listId: list.id } }" :to="{ name: 'filter.settings.delete', params: { projectId: project.id } }"
icon="trash-alt" icon="trash-alt"
> >
{{ $t('misc.delete') }} {{ $t('misc.delete') }}
</dropdown-item> </dropdown-item>
</template> </template>
<template v-else-if="list.isArchived"> <template v-else-if="project.isArchived">
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.archive', params: { listId: list.id } }" :to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
icon="archive" icon="archive"
> >
{{ $t('menu.unarchive') }} {{ $t('menu.unarchive') }}
@ -33,32 +33,32 @@
</template> </template>
<template v-else> <template v-else>
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.edit', params: { listId: list.id } }" :to="{ name: 'project.settings.edit', params: { projectId: project.id } }"
icon="pen" icon="pen"
> >
{{ $t('menu.edit') }} {{ $t('menu.edit') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
v-if="backgroundsEnabled" v-if="backgroundsEnabled"
:to="{ name: 'list.settings.background', params: { listId: list.id } }" :to="{ name: 'project.settings.background', params: { projectId: project.id } }"
icon="image" icon="image"
> >
{{ $t('menu.setBackground') }} {{ $t('menu.setBackground') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.share', params: { listId: list.id } }" :to="{ name: 'project.settings.share', params: { projectId: project.id } }"
icon="share-alt" icon="share-alt"
> >
{{ $t('menu.share') }} {{ $t('menu.share') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }" :to="{ name: 'project.settings.duplicate', params: { projectId: project.id } }"
icon="paste" icon="paste"
> >
{{ $t('menu.duplicate') }} {{ $t('menu.duplicate') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.archive', params: { listId: list.id } }" :to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
icon="archive" icon="archive"
> >
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
@ -66,14 +66,14 @@
<Subscription <Subscription
class="has-no-shadow" class="has-no-shadow"
:is-button="false" :is-button="false"
entity="list" entity="project"
:entity-id="list.id" :entity-id="project.id"
:model-value="list.subscription" :model-value="project.subscription"
@update:model-value="setSubscriptionInStore" @update:model-value="setSubscriptionInStore"
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.delete', params: { listId: list.id } }" :to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
icon="trash-alt" icon="trash-alt"
class="has-text-danger" class="has-text-danger"
> >
@ -90,26 +90,26 @@ import BaseButton from '@/components/base/BaseButton.vue'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import Subscription from '@/components/misc/subscription.vue' import Subscription from '@/components/misc/subscription.vue'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
import {isSavedFilter} from '@/services/savedFilter' import {isSavedFilter} from '@/services/savedFilter'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useListStore} from '@/stores/lists' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
list: { project: {
type: Object as PropType<IList>, type: Object as PropType<IProject>,
required: true, required: true,
}, },
}) })
const listStore = useListStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null) const subscription = ref<ISubscription | null>(null)
watchEffect(() => { watchEffect(() => {
subscription.value = props.list.subscription ?? null subscription.value = props.project.subscription ?? null
}) })
const configStore = useConfigStore() const configStore = useConfigStore()
@ -117,11 +117,11 @@ const backgroundsEnabled = computed(() => configStore.enabledBackgroundProviders
function setSubscriptionInStore(sub: ISubscription) { function setSubscriptionInStore(sub: ISubscription) {
subscription.value = sub subscription.value = sub
const updatedList = { const updatedProject = {
...props.list, ...props.project,
subscription: sub, subscription: sub,
} }
listStore.setList(updatedList) projectStore.setProject(updatedProject)
namespaceStore.setListInNamespaceById(updatedList) namespaceStore.setProjectInNamespaceById(updatedProject)
} }
</script> </script>

View File

@ -63,18 +63,18 @@ import TeamService from '@/services/team'
import NamespaceModel from '@/models/namespace' import NamespaceModel from '@/models/namespace'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue' import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {getHistory} from '@/modules/listHistory' import {getHistory} from '@/modules/projectHistory'
import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText' import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {success} from '@/message' import {success} from '@/message'
@ -82,13 +82,13 @@ import {success} from '@/message'
import type {ITeam} from '@/modelTypes/ITeam' import type {ITeam} from '@/modelTypes/ITeam'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const router = useRouter() const router = useRouter()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const listStore = useListStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const labelStore = useLabelStore() const labelStore = useLabelStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
@ -98,13 +98,13 @@ type DoAction<Type = any> = { type: ACTION_TYPE } & Type
enum ACTION_TYPE { enum ACTION_TYPE {
CMD = 'cmd', CMD = 'cmd',
TASK = 'task', TASK = 'task',
LIST = 'list', PROJECT = 'project',
TEAM = 'team', TEAM = 'team',
} }
enum COMMAND_TYPE { enum COMMAND_TYPE {
NEW_TASK = 'newTask', NEW_TASK = 'newTask',
NEW_LIST = 'newList', NEW_PROJECT = 'newProject',
NEW_NAMESPACE = 'newNamespace', NEW_NAMESPACE = 'newNamespace',
NEW_TEAM = 'newTeam', NEW_TEAM = 'newTeam',
} }
@ -112,7 +112,7 @@ enum COMMAND_TYPE {
enum SEARCH_MODE { enum SEARCH_MODE {
ALL = 'all', ALL = 'all',
TASKS = 'tasks', TASKS = 'tasks',
LISTS = 'lists', PROJECTS = 'projects',
TEAMS = 'teams', TEAMS = 'teams',
} }
@ -137,26 +137,26 @@ function closeQuickActions() {
baseStore.setQuickActionsActive(false) baseStore.setQuickActionsActive(false)
} }
const foundLists = computed(() => { const foundProjects = computed(() => {
const { list } = parsedQuery.value const { project } = parsedQuery.value
if ( if (
searchMode.value === SEARCH_MODE.ALL || searchMode.value === SEARCH_MODE.ALL ||
searchMode.value === SEARCH_MODE.LISTS || searchMode.value === SEARCH_MODE.PROJECTS ||
list === null project === null
) { ) {
return [] return []
} }
const ncache: { [id: ListModel['id']]: INamespace } = {} const ncache: { [id: ProjectModel['id']]: INamespace } = {}
const history = getHistory() const history = getHistory()
const allLists = [ const allProjects = [
...new Set([ ...new Set([
...history.map((l) => listStore.getListById(l.id)), ...history.map((l) => projectStore.getProjectById(l.id)),
...listStore.searchList(list), ...projectStore.searchProject(project),
]), ]),
] ]
return allLists.filter((l) => { return allProjects.filter((l) => {
if (typeof l === 'undefined' || l === null) { if (typeof l === 'undefined' || l === null) {
return false return false
} }
@ -191,9 +191,9 @@ const results = computed<Result[]>(() => {
items: foundTasks.value, items: foundTasks.value,
}, },
{ {
type: ACTION_TYPE.LIST, type: ACTION_TYPE.PROJECT,
title: t('quickActions.lists'), title: t('quickActions.projects'),
items: foundLists.value, items: foundProjects.value,
}, },
{ {
type: ACTION_TYPE.TEAM, type: ACTION_TYPE.TEAM,
@ -206,7 +206,7 @@ const results = computed<Result[]>(() => {
const loading = computed(() => const loading = computed(() =>
taskService.loading || taskService.loading ||
namespaceStore.isLoading || namespaceStore.isLoading ||
listStore.isLoading || projectStore.isLoading ||
teamService.loading, teamService.loading,
) )
@ -224,11 +224,11 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
placeholder: t('quickActions.newTask'), placeholder: t('quickActions.newTask'),
action: newTask, action: newTask,
}, },
newList: { newProject: {
type: COMMAND_TYPE.NEW_LIST, type: COMMAND_TYPE.NEW_PROJECT,
title: t('quickActions.cmds.newList'), title: t('quickActions.cmds.newProject'),
placeholder: t('quickActions.newList'), placeholder: t('quickActions.newProject'),
action: newList, action: newProject,
}, },
newNamespace: { newNamespace: {
type: COMMAND_TYPE.NEW_NAMESPACE, type: COMMAND_TYPE.NEW_NAMESPACE,
@ -246,24 +246,24 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
const placeholder = computed(() => selectedCmd.value?.placeholder || t('quickActions.placeholder')) const placeholder = computed(() => selectedCmd.value?.placeholder || t('quickActions.placeholder'))
const currentList = computed(() => Object.keys(baseStore.currentList).length === 0 const currentProject = computed(() => Object.keys(baseStore.currentProject).length === 0
? null ? null
: baseStore.currentList, : baseStore.currentProject,
) )
const hintText = computed(() => { const hintText = computed(() => {
let namespace let namespace
if (selectedCmd.value !== null && currentList.value !== null) { if (selectedCmd.value !== null && currentProject.value !== null) {
switch (selectedCmd.value.type) { switch (selectedCmd.value.type) {
case COMMAND_TYPE.NEW_TASK: case COMMAND_TYPE.NEW_TASK:
return t('quickActions.createTask', { return t('quickActions.createTask', {
title: currentList.value.title, title: currentProject.value.title,
}) })
case COMMAND_TYPE.NEW_LIST: case COMMAND_TYPE.NEW_PROJECT:
namespace = namespaceStore.getNamespaceById( namespace = namespaceStore.getNamespaceById(
currentList.value.namespaceId, currentProject.value.namespaceId,
) )
return t('quickActions.createList', { return t('quickActions.createProject', {
title: namespace?.title, title: namespace?.title,
}) })
} }
@ -275,8 +275,8 @@ const hintText = computed(() => {
const availableCmds = computed(() => { const availableCmds = computed(() => {
const cmds = [] const cmds = []
if (currentList.value !== null) { if (currentProject.value !== null) {
cmds.push(commands.value.newTask, commands.value.newList) cmds.push(commands.value.newTask, commands.value.newProject)
} }
cmds.push(commands.value.newNamespace, commands.value.newTeam) cmds.push(commands.value.newNamespace, commands.value.newTeam)
return cmds return cmds
@ -288,21 +288,21 @@ const searchMode = computed(() => {
if (query.value === '') { if (query.value === '') {
return SEARCH_MODE.ALL return SEARCH_MODE.ALL
} }
const { text, list, labels, assignees } = parsedQuery.value const { text, project, labels, assignees } = parsedQuery.value
if (assignees.length === 0 && text !== '') { if (assignees.length === 0 && text !== '') {
return SEARCH_MODE.TASKS return SEARCH_MODE.TASKS
} }
if ( if (
assignees.length === 0 && assignees.length === 0 &&
list !== null && project !== null &&
text === '' && text === '' &&
labels.length === 0 labels.length === 0
) { ) {
return SEARCH_MODE.LISTS return SEARCH_MODE.PROJECTS
} }
if ( if (
assignees.length > 0 && assignees.length > 0 &&
list === null && project === null &&
text === '' && text === '' &&
labels.length === 0 labels.length === 0
) { ) {
@ -356,7 +356,7 @@ function searchTasks() {
taskSearchTimeout.value = null taskSearchTimeout.value = null
} }
const { text, list: listName, labels } = parsedQuery.value const { text, project: projectName, labels } = parsedQuery.value
const filters: Filter[] = [] const filters: Filter[] = []
@ -373,10 +373,10 @@ function searchTasks() {
}) })
} }
if (listName !== null) { if (projectName !== null) {
const list = listStore.findListByExactname(listName) const project = projectStore.findProjectByExactname(projectName)
if (list !== null) { if (project !== null) {
addFilter('listId', list.id, 'equals') addFilter('projectId', project.id, 'equals')
} }
} }
@ -396,9 +396,9 @@ function searchTasks() {
const r = await taskService.getAll({}, params) as DoAction<ITask>[] const r = await taskService.getAll({}, params) as DoAction<ITask>[]
foundTasks.value = r.map((t) => { foundTasks.value = r.map((t) => {
t.type = ACTION_TYPE.TASK t.type = ACTION_TYPE.TASK
const list = listStore.getListById(t.listId) const project = projectStore.getProjectById(t.projectId)
if (list !== null) { if (project !== null) {
t.title = `${t.title} (${list.title})` t.title = `${t.title} (${project.title})`
} }
return t return t
}) })
@ -444,11 +444,11 @@ const searchInput = ref<HTMLElement | null>(null)
async function doAction(type: ACTION_TYPE, item: DoAction) { async function doAction(type: ACTION_TYPE, item: DoAction) {
switch (type) { switch (type) {
case ACTION_TYPE.LIST: case ACTION_TYPE.PROJECT:
closeQuickActions() closeQuickActions()
await router.push({ await router.push({
name: 'list.index', name: 'project.index',
params: { listId: (item as DoAction<IList>).id }, params: { projectId: (item as DoAction<IProject>).id },
}) })
break break
case ACTION_TYPE.TASK: case ACTION_TYPE.TASK:
@ -489,29 +489,29 @@ async function doCmd() {
} }
async function newTask() { async function newTask() {
if (currentList.value === null) { if (currentProject.value === null) {
return return
} }
const task = await taskStore.createNewTask({ const task = await taskStore.createNewTask({
title: query.value, title: query.value,
listId: currentList.value.id, projectId: currentProject.value.id,
}) })
success({ message: t('task.createSuccess') }) success({ message: t('task.createSuccess') })
await router.push({ name: 'task.detail', params: { id: task.id } }) await router.push({ name: 'task.detail', params: { id: task.id } })
} }
async function newList() { async function newProject() {
if (currentList.value === null) { if (currentProject.value === null) {
return return
} }
const newList = await listStore.createList(new ListModel({ const newProject = await projectStore.createProject(new ProjectModel({
title: query.value, title: query.value,
namespaceId: currentList.value.namespaceId, namespaceId: currentProject.value.namespaceId,
})) }))
success({ message: t('list.create.createdSuccess')}) success({ message: t('project.create.createdSuccess')})
await router.push({ await router.push({
name: 'list.index', name: 'project.index',
params: { listId: newList.id }, params: { projectId: newProject.id },
}) })
} }

View File

@ -1,39 +1,39 @@
<template> <template>
<div> <div>
<p class="has-text-weight-bold"> <p class="has-text-weight-bold">
{{ $t('list.share.links.title') }} {{ $t('project.share.links.title') }}
<span <span
class="is-size-7 has-text-grey is-italic ml-3" class="is-size-7 has-text-grey is-italic ml-3"
v-tooltip="$t('list.share.links.explanation')"> v-tooltip="$t('project.share.links.explanation')">
{{ $t('list.share.links.what') }} {{ $t('project.share.links.what') }}
</span> </span>
</p> </p>
<div class="sharables-list"> <div class="sharables-project">
<x-button <x-button
v-if="!(linkShares.length === 0 || showNewForm)" v-if="!(linkShares.length === 0 || showNewForm)"
@click="showNewForm = true" @click="showNewForm = true"
icon="plus" icon="plus"
class="mb-4"> class="mb-4">
{{ $t('list.share.links.create') }} {{ $t('project.share.links.create') }}
</x-button> </x-button>
<div class="p-4" v-if="linkShares.length === 0 || showNewForm"> <div class="p-4" v-if="linkShares.length === 0 || showNewForm">
<div class="field"> <div class="field">
<label class="label" for="linkShareRight"> <label class="label" for="linkShareRight">
{{ $t('list.share.right.title') }} {{ $t('project.share.right.title') }}
</label> </label>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select v-model="selectedRight" id="linkShareRight"> <select v-model="selectedRight" id="linkShareRight">
<option :value="RIGHTS.READ"> <option :value="RIGHTS.READ">
{{ $t('list.share.right.read') }} {{ $t('project.share.right.read') }}
</option> </option>
<option :value="RIGHTS.READ_WRITE"> <option :value="RIGHTS.READ_WRITE">
{{ $t('list.share.right.readWrite') }} {{ $t('project.share.right.readWrite') }}
</option> </option>
<option :value="RIGHTS.ADMIN"> <option :value="RIGHTS.ADMIN">
{{ $t('list.share.right.admin') }} {{ $t('project.share.right.admin') }}
</option> </option>
</select> </select>
</div> </div>
@ -41,21 +41,21 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="linkShareName"> <label class="label" for="linkShareName">
{{ $t('list.share.links.name') }} {{ $t('project.share.links.name') }}
</label> </label>
<div class="control"> <div class="control">
<input <input
id="linkShareName" id="linkShareName"
class="input" class="input"
:placeholder="$t('list.share.links.namePlaceholder')" :placeholder="$t('project.share.links.namePlaceholder')"
v-tooltip="$t('list.share.links.nameExplanation')" v-tooltip="$t('project.share.links.nameExplanation')"
v-model="name" v-model="name"
/> />
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="linkSharePassword"> <label class="label" for="linkSharePassword">
{{ $t('list.share.links.password') }} {{ $t('project.share.links.password') }}
</label> </label>
<div class="control"> <div class="control">
<input <input
@ -63,25 +63,25 @@
type="password" type="password"
class="input" class="input"
:placeholder="$t('user.auth.passwordPlaceholder')" :placeholder="$t('user.auth.passwordPlaceholder')"
v-tooltip="$t('list.share.links.passwordExplanation')" v-tooltip="$t('project.share.links.passwordExplanation')"
v-model="password" v-model="password"
/> />
</div> </div>
</div> </div>
<x-button @click="add(listId)" icon="plus"> <x-button @click="add(projectId)" icon="plus">
{{ $t('list.share.share') }} {{ $t('project.share.share') }}
</x-button> </x-button>
</div> </div>
<table <table
class="table has-actions is-striped is-hoverable is-fullwidth link-share-list" class="table has-actions is-striped is-hoverable is-fullwidth"
v-if="linkShares.length > 0" v-if="linkShares.length > 0"
> >
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th>{{ $t('list.share.links.view') }}</th> <th>{{ $t('project.share.links.view') }}</th>
<th>{{ $t('list.share.attributes.delete') }}</th> <th>{{ $t('project.share.attributes.delete') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -92,7 +92,7 @@
</p> </p>
<p class="mb-2"> <p class="mb-2">
<i18n-t keypath="list.share.links.sharedBy" scope="global"> <i18n-t keypath="project.share.links.sharedBy" scope="global">
<strong>{{ getDisplayName(s.sharedBy) }}</strong> <strong>{{ getDisplayName(s.sharedBy) }}</strong>
</i18n-t> </i18n-t>
</p> </p>
@ -102,19 +102,19 @@
<span class="icon is-small"> <span class="icon is-small">
<icon icon="lock"/> <icon icon="lock"/>
</span>&nbsp; </span>&nbsp;
{{ $t('list.share.right.admin') }} {{ $t('project.share.right.admin') }}
</template> </template>
<template v-else-if="s.right === RIGHTS.READ_WRITE"> <template v-else-if="s.right === RIGHTS.READ_WRITE">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="pen"/> <icon icon="pen"/>
</span>&nbsp; </span>&nbsp;
{{ $t('list.share.right.readWrite') }} {{ $t('project.share.right.readWrite') }}
</template> </template>
<template v-else> <template v-else>
<span class="icon is-small"> <span class="icon is-small">
<icon icon="users"/> <icon icon="users"/>
</span>&nbsp; </span>&nbsp;
{{ $t('list.share.right.read') }} {{ $t('project.share.right.read') }}
</template> </template>
</p> </p>
@ -172,14 +172,14 @@
<modal <modal
:enabled="showDeleteModal" :enabled="showDeleteModal"
@close="showDeleteModal = false" @close="showDeleteModal = false"
@submit="remove(listId)" @submit="remove(projectId)"
> >
<template #header> <template #header>
<span>{{ $t('list.share.links.remove') }}</span> <span>{{ $t('project.share.links.remove') }}</span>
</template> </template>
<template #text> <template #text>
<p>{{ $t('list.share.links.removeText') }}</p> <p>{{ $t('project.share.links.removeText') }}</p>
</template> </template>
</modal> </modal>
</div> </div>
@ -193,19 +193,19 @@ import {RIGHTS} from '@/constants/rights'
import LinkShareModel from '@/models/linkShare' import LinkShareModel from '@/models/linkShare'
import type {ILinkShare} from '@/modelTypes/ILinkShare' import type {ILinkShare} from '@/modelTypes/ILinkShare'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import LinkShareService from '@/services/linkShare' 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 {ListView} from '@/types/ListView' import type {ProjectView} from '@/types/ProjectView'
import {LIST_VIEWS} from '@/types/ListView' import {PROJECT_VIEWS} from '@/types/ProjectView'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
const props = defineProps({ const props = defineProps({
listId: { projectId: {
default: 0, default: 0,
required: true, required: true,
}, },
@ -222,20 +222,20 @@ const showDeleteModal = ref(false)
const linkIdToDelete = ref(0) const linkIdToDelete = ref(0)
const showNewForm = ref(false) const showNewForm = ref(false)
type SelectedViewMapper = Record<IList['id'], ListView> type SelectedViewMapper = Record<IProject['id'], ProjectView>
const selectedView = ref<SelectedViewMapper>({}) const selectedView = ref<SelectedViewMapper>({})
const availableViews = computed<Record<ListView, string>>(() => ({ const availableViews = computed<Record<ProjectView, string>>(() => ({
list: t('list.list.title'), list: t('project.list.title'),
gantt: t('list.gantt.title'), gantt: t('project.gantt.title'),
table: t('list.table.title'), table: t('project.table.title'),
kanban: t('list.kanban.title'), kanban: t('project.kanban.title'),
})) }))
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
watch( watch(
() => props.listId, () => props.projectId,
load, load,
{immediate: true}, {immediate: true},
) )
@ -243,23 +243,23 @@ watch(
const configStore = useConfigStore() const configStore = useConfigStore()
const frontendUrl = computed(() => configStore.frontendUrl) const frontendUrl = computed(() => configStore.frontendUrl)
async function load(listId: IList['id']) { async function load(projectId: IProject['id']) {
// If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here // If projectId == 0 the project on the calling component wasn't already loaded, so we just bail out here
if (listId === 0) { if (projectId === 0) {
return return
} }
const links = await linkShareService.getAll({listId}) const links = await linkShareService.getAll({projectId})
links.forEach((l: ILinkShare) => { links.forEach((l: ILinkShare) => {
selectedView.value[l.id] = 'list' selectedView.value[l.id] = 'project'
}) })
linkShares.value = links linkShares.value = links
} }
async function add(listId: IList['id']) { async function add(projectId: IProject['id']) {
const newLinkShare = new LinkShareModel({ const newLinkShare = new LinkShareModel({
right: selectedRight.value, right: selectedRight.value,
listId, projectId,
name: name.value, name: name.value,
password: password.value, password: password.value,
}) })
@ -268,31 +268,31 @@ async function add(listId: IList['id']) {
name.value = '' name.value = ''
password.value = '' password.value = ''
showNewForm.value = false showNewForm.value = false
success({message: t('list.share.links.createSuccess')}) success({message: t('project.share.links.createSuccess')})
await load(listId) await load(projectId)
} }
async function remove(listId: IList['id']) { async function remove(projectId: IProject['id']) {
try { try {
await linkShareService.delete(new LinkShareModel({ await linkShareService.delete(new LinkShareModel({
id: linkIdToDelete.value, id: linkIdToDelete.value,
listId, projectId,
})) }))
success({message: t('list.share.links.deleteSuccess')}) success({message: t('project.share.links.deleteSuccess')})
await load(listId) await load(projectId)
} finally { } finally {
showDeleteModal.value = false showDeleteModal.value = false
} }
} }
function getShareLink(hash: string, view: ListView = LIST_VIEWS.LIST) { function getShareLink(hash: string, view: ProjectView = PROJECT_VIEWS.LIST) {
return frontendUrl.value + 'share/' + hash + '/auth?view=' + view return frontendUrl.value + 'share/' + hash + '/auth?view=' + view
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// FIXME: I think this is not needed // FIXME: I think this is not needed
.sharables-list:not(.card-content) { .sharables-project:not(.card-content) {
overflow-y: auto overflow-y: auto
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<p class="has-text-weight-bold"> <p class="has-text-weight-bold">
{{ $t('list.share.userTeam.shared', {type: shareTypeNames}) }} {{ $t('project.share.userTeam.shared', {type: shareTypeNames}) }}
</p> </p>
<div v-if="userIsAdmin"> <div v-if="userIsAdmin">
<div class="field has-addons"> <div class="field has-addons">
@ -19,7 +19,7 @@
/> />
</p> </p>
<p class="control"> <p class="control">
<x-button @click="add()">{{ $t('list.share.share') }}</x-button> <x-button @click="add()">{{ $t('project.share.share') }}</x-button>
</p> </p>
</div> </div>
</div> </div>
@ -31,7 +31,7 @@
<td>{{ getDisplayName(s) }}</td> <td>{{ getDisplayName(s) }}</td>
<td> <td>
<template v-if="s.id === userInfo.id"> <template v-if="s.id === userInfo.id">
<b class="is-success">{{ $t('list.share.userTeam.you') }}</b> <b class="is-success">{{ $t('project.share.userTeam.you') }}</b>
</template> </template>
</td> </td>
</template> </template>
@ -52,19 +52,19 @@
<span class="icon is-small"> <span class="icon is-small">
<icon icon="lock"/> <icon icon="lock"/>
</span> </span>
{{ $t('list.share.right.admin') }} {{ $t('project.share.right.admin') }}
</template> </template>
<template v-else-if="s.right === RIGHTS.READ_WRITE"> <template v-else-if="s.right === RIGHTS.READ_WRITE">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="pen"/> <icon icon="pen"/>
</span> </span>
{{ $t('list.share.right.readWrite') }} {{ $t('project.share.right.readWrite') }}
</template> </template>
<template v-else> <template v-else>
<span class="icon is-small"> <span class="icon is-small">
<icon icon="users"/> <icon icon="users"/>
</span> </span>
{{ $t('list.share.right.read') }} {{ $t('project.share.right.read') }}
</template> </template>
</td> </td>
<td class="actions" v-if="userIsAdmin"> <td class="actions" v-if="userIsAdmin">
@ -78,19 +78,19 @@
:selected="s.right === RIGHTS.READ" :selected="s.right === RIGHTS.READ"
:value="RIGHTS.READ" :value="RIGHTS.READ"
> >
{{ $t('list.share.right.read') }} {{ $t('project.share.right.read') }}
</option> </option>
<option <option
:selected="s.right === RIGHTS.READ_WRITE" :selected="s.right === RIGHTS.READ_WRITE"
:value="RIGHTS.READ_WRITE" :value="RIGHTS.READ_WRITE"
> >
{{ $t('list.share.right.readWrite') }} {{ $t('project.share.right.readWrite') }}
</option> </option>
<option <option
:selected="s.right === RIGHTS.ADMIN" :selected="s.right === RIGHTS.ADMIN"
:value="RIGHTS.ADMIN" :value="RIGHTS.ADMIN"
> >
{{ $t('list.share.right.admin') }} {{ $t('project.share.right.admin') }}
</option> </option>
</select> </select>
</div> </div>
@ -110,7 +110,7 @@
</table> </table>
<nothing v-else> <nothing v-else>
{{ $t('list.share.userTeam.notShared', {type: shareTypeNames}) }} {{ $t('project.share.userTeam.notShared', {type: shareTypeNames}) }}
</nothing> </nothing>
<modal <modal
@ -120,11 +120,11 @@
> >
<template #header> <template #header>
<span>{{ <span>{{
$t('list.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName}) $t('project.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
}}</span> }}</span>
</template> </template>
<template #text> <template #text>
<p>{{ $t('list.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p> <p>{{ $t('project.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
</template> </template>
</modal> </modal>
</div> </div>
@ -143,9 +143,9 @@ import UserNamespaceService from '@/services/userNamespace'
import UserNamespaceModel from '@/models/userNamespace' import UserNamespaceModel from '@/models/userNamespace'
import type {IUserNamespace} from '@/modelTypes/IUserNamespace' import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
import UserListService from '@/services/userList' import UserProjectService from '@/services/userProject'
import UserListModel from '@/models/userList' import UserProjectModel from '@/models/userProject'
import type {IUserList} from '@/modelTypes/IUserList' import type {IUserProject} from '@/modelTypes/IUserProject'
import UserService from '@/services/user' import UserService from '@/services/user'
import UserModel, { getDisplayName } from '@/models/user' import UserModel, { getDisplayName } from '@/models/user'
@ -155,9 +155,9 @@ import TeamNamespaceService from '@/services/teamNamespace'
import TeamNamespaceModel from '@/models/teamNamespace' import TeamNamespaceModel from '@/models/teamNamespace'
import type { ITeamNamespace } from '@/modelTypes/ITeamNamespace' import type { ITeamNamespace } from '@/modelTypes/ITeamNamespace'
import TeamListService from '@/services/teamList' import TeamProjectService from '@/services/teamProject'
import TeamListModel from '@/models/teamList' import TeamProjectModel from '@/models/teamProject'
import type { ITeamList } from '@/modelTypes/ITeamList' import type { ITeamProject } from '@/modelTypes/ITeamProject'
import TeamService from '@/services/team' import TeamService from '@/services/team'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
@ -172,7 +172,7 @@ import {useAuthStore} from '@/stores/auth'
const props = defineProps({ const props = defineProps({
type: { type: {
type: String as PropType<'list' | 'namespace'>, type: String as PropType<'project' | 'namespace'>,
default: '', default: '',
}, },
shareType: { shareType: {
@ -191,9 +191,9 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
// This user service is either a userNamespaceService or a userListService, depending on the type we are using // This user service is either a userNamespaceService or a userProjectService, depending on the type we are using
let stuffService: UserNamespaceService | UserListService | TeamListService | TeamNamespaceService let stuffService: UserNamespaceService | UserProjectService | TeamProjectService | TeamNamespaceService
let stuffModel: IUserNamespace | IUserList | ITeamList | ITeamNamespace let stuffModel: IUserNamespace | IUserProject | ITeamProject | ITeamNamespace
let searchService: UserService | TeamService let searchService: UserService | TeamService
let sharable: Ref<IUser | ITeam> let sharable: Ref<IUser | ITeam>
@ -201,7 +201,7 @@ const searchLabel = ref('')
const selectedRight = ref({}) const selectedRight = ref({})
// This holds either teams or users who this namepace or list is shared with // This holds either teams or users who this namepace or project is shared with
const sharables = ref([]) const sharables = ref([])
const showDeleteModal = ref(false) const showDeleteModal = ref(false)
@ -212,11 +212,11 @@ const userInfo = computed(() => authStore.info)
function createShareTypeNameComputed(count: number) { function createShareTypeNameComputed(count: number) {
return computed(() => { return computed(() => {
if (props.shareType === 'user') { if (props.shareType === 'user') {
return t('list.share.userTeam.typeUser', count) return t('project.share.userTeam.typeUser', count)
} }
if (props.shareType === 'team') { if (props.shareType === 'team') {
return t('list.share.userTeam.typeTeam', count) return t('project.share.userTeam.typeTeam', count)
} }
return '' return ''
@ -227,8 +227,8 @@ const shareTypeNames = createShareTypeNameComputed(2)
const shareTypeName = createShareTypeNameComputed(1) const shareTypeName = createShareTypeNameComputed(1)
const sharableName = computed(() => { const sharableName = computed(() => {
if (props.type === 'list') { if (props.type === 'project') {
return t('list.list.title') return t('project.list.title')
} }
if (props.shareType === 'namespace') { if (props.shareType === 'namespace') {
@ -244,9 +244,9 @@ if (props.shareType === 'user') {
sharable = ref(new UserModel()) sharable = ref(new UserModel())
searchLabel.value = 'username' searchLabel.value = 'username'
if (props.type === 'list') { if (props.type === 'project') {
stuffService = shallowReactive(new UserListService()) stuffService = shallowReactive(new UserProjectService())
stuffModel = reactive(new UserListModel({listId: props.id})) stuffModel = reactive(new UserProjectModel({projectId: props.id}))
} else if (props.type === 'namespace') { } else if (props.type === 'namespace') {
stuffService = shallowReactive(new UserNamespaceService()) stuffService = shallowReactive(new UserNamespaceService())
stuffModel = reactive(new UserNamespaceModel({ stuffModel = reactive(new UserNamespaceModel({
@ -261,9 +261,9 @@ if (props.shareType === 'user') {
sharable = ref(new TeamModel()) sharable = ref(new TeamModel())
searchLabel.value = 'name' searchLabel.value = 'name'
if (props.type === 'list') { if (props.type === 'project') {
stuffService = shallowReactive(new TeamListService()) stuffService = shallowReactive(new TeamProjectService())
stuffModel = reactive(new TeamListModel({listId: props.id})) stuffModel = reactive(new TeamProjectModel({projectId: props.id}))
} else if (props.type === 'namespace') { } else if (props.type === 'namespace') {
stuffService = shallowReactive(new TeamNamespaceService()) stuffService = shallowReactive(new TeamNamespaceService())
stuffModel = reactive(new TeamNamespaceModel({ stuffModel = reactive(new TeamNamespaceModel({
@ -303,7 +303,7 @@ async function deleteSharable() {
} }
} }
success({ success({
message: t('list.share.userTeam.removeSuccess', { message: t('project.share.userTeam.removeSuccess', {
type: shareTypeName.value, type: shareTypeName.value,
sharable: sharableName.value, sharable: sharableName.value,
}), }),
@ -326,7 +326,7 @@ async function add(admin) {
} }
await stuffService.create(stuffModel) await stuffService.create(stuffModel)
success({message: t('list.share.userTeam.addedSuccess', {type: shareTypeName.value})}) success({message: t('project.share.userTeam.addedSuccess', {type: shareTypeName.value})})
await load() await load()
} }
@ -358,7 +358,7 @@ async function toggleType(sharable) {
sharables.value[i].right = r.right sharables.value[i].right = r.right
} }
} }
success({message: t('list.share.userTeam.updatedSuccess', {type: shareTypeName.value})}) success({message: t('project.share.userTeam.updatedSuccess', {type: shareTypeName.value})})
} }
const found = ref([]) const found = ref([])

View File

@ -50,7 +50,7 @@ import {parseKebabDate} from '@/helpers/time/parseKebabDate'
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask' import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
import type {DateISO} from '@/types/DateISO' import type {DateISO} from '@/types/DateISO'
import type {GanttFilters} from '@/views/list/helpers/useGanttFilters' import type {GanttFilters} from '@/views/project/helpers/useGanttFilters'
import { import {
extendDayjs, extendDayjs,

View File

@ -5,7 +5,7 @@
<textarea <textarea
class="add-task-textarea input" class="add-task-textarea input"
:class="{'textarea-empty': newTaskTitle === ''}" :class="{'textarea-empty': newTaskTitle === ''}"
:placeholder="$t('list.list.addPlaceholder')" :placeholder="$t('project.list.addPlaceholder')"
rows="1" rows="1"
v-focus v-focus
v-model="newTaskTitle" v-model="newTaskTitle"
@ -24,10 +24,10 @@
@click="addTask()" @click="addTask()"
icon="plus" icon="plus"
:loading="loading" :loading="loading"
:aria-label="$t('list.list.add')" :aria-label="$t('project.list.add')"
> >
<span class="button-text"> <span class="button-text">
{{ $t('list.list.add') }} {{ $t('project.list.add') }}
</span> </span>
</x-button> </x-button>
</p> </p>
@ -107,7 +107,7 @@ const loading = computed(() => taskStore.isLoading)
async function addTask() { async function addTask() {
if (newTaskTitle.value === '') { if (newTaskTitle.value === '') {
errorMessage.value = t('list.create.addTitleRequired') errorMessage.value = t('project.create.addTitleRequired')
return return
} }
errorMessage.value = '' errorMessage.value = ''
@ -128,20 +128,20 @@ async function addTask() {
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title) ?? []) const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title) ?? [])
await taskStore.ensureLabelsExist(allLabels.flat()) await taskStore.ensureLabelsExist(allLabels.flat())
const newTasks = tasksToCreate.map(async ({title, list}) => { const newTasks = tasksToCreate.map(async ({title, project}) => {
if (title === '') { if (title === '') {
return return
} }
// If the task has a list specified, make sure to use it // If the task has a project specified, make sure to use it
let listId = null let projectId = null
if (list !== null) { if (project !== null) {
listId = await taskStore.findListId({list, listId: 0}) projectId = await taskStore.findProjectId({project, projectId: 0})
} }
const task = await taskStore.createNewTask({ const task = await taskStore.createNewTask({
title, title,
listId: listId || authStore.settings.defaultListId, projectId: projectId || authStore.settings.defaultProjectId,
position: props.defaultPosition, position: props.defaultPosition,
}) })
createdTasks[title] = task createdTasks[title] = task
@ -176,7 +176,7 @@ async function addTask() {
})) }))
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [createdParentTask] createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [createdParentTask]
// we're only emitting here so that the relation shows up in the task list // we're only emitting here so that the relation shows up in the project
emit('taskAdded', createdTask) emit('taskAdded', createdTask)
return rel return rel
@ -184,8 +184,8 @@ async function addTask() {
await Promise.all(relations) await Promise.all(relations)
} catch (e: any) { } catch (e: any) {
newTaskTitle.value = taskTitleBackup newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_LIST') { if (e?.message === 'NO_PROJECT') {
errorMessage.value = t('list.create.addListRequired') errorMessage.value = t('project.create.addProjectRequired')
return return
} }
throw e throw e

View File

@ -1,6 +1,6 @@
<template> <template>
<Multiselect <Multiselect
:loading="listUserService.loading" :loading="projectUserService.loading"
:placeholder="$t('task.assignee.placeholder')" :placeholder="$t('task.assignee.placeholder')"
:multiple="true" :multiple="true"
@search="findUser" @search="findUser"
@ -30,7 +30,7 @@ import Multiselect from '@/components/input/multiselect.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {includesById} from '@/helpers/utils' import {includesById} from '@/helpers/utils'
import ListUserService from '@/services/listUsers' import ProjectUserService from '@/services/projectUsers'
import {success} from '@/message' import {success} from '@/message'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
@ -42,7 +42,7 @@ const props = defineProps({
type: Number, type: Number,
required: true, required: true,
}, },
listId: { projectId: {
type: Number, type: Number,
required: true, required: true,
}, },
@ -59,7 +59,7 @@ const emit = defineEmits(['update:modelValue'])
const taskStore = useTaskStore() const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const listUserService = shallowReactive(new ListUserService()) const projectUserService = shallowReactive(new ProjectUserService())
const foundUsers = ref<IUser[]>([]) const foundUsers = ref<IUser[]>([])
const assignees = ref<IUser[]>([]) const assignees = ref<IUser[]>([])
let isAdding = false let isAdding = false
@ -94,7 +94,7 @@ async function addAssignee(user: IUser) {
async function removeAssignee(user: IUser) { async function removeAssignee(user: IUser) {
await taskStore.removeAssignee({user: user, taskId: props.taskId}) await taskStore.removeAssignee({user: user, taskId: props.taskId})
// Remove the assignee from the list // Remove the assignee from the project
for (const a in assignees.value) { for (const a in assignees.value) {
if (assignees.value[a].id === user.id) { if (assignees.value[a].id === user.id) {
assignees.value.splice(a, 1) assignees.value.splice(a, 1)
@ -109,7 +109,7 @@ async function findUser(query: string) {
return return
} }
const response = await listUserService.getAll({listId: props.listId}, {s: query}) as IUser[] const response = await projectUserService.getAll({projectId: props.projectId}, {s: query}) as IUser[]
// Filter the results to not include users who are already assigned // Filter the results to not include users who are already assigned
foundUsers.value = response foundUsers.value = response

View File

@ -1,18 +1,18 @@
<template> <template>
<Multiselect <Multiselect
class="control is-expanded" class="control is-expanded"
:placeholder="$t('list.search')" :placeholder="$t('project.search')"
:search-results="foundLists" :search-results="foundProjects"
label="title" label="title"
:select-placeholder="$t('list.searchSelect')" :select-placeholder="$t('project.searchSelect')"
:model-value="list" :model-value="project"
@update:model-value="Object.assign(list, $event)" @update:model-value="Object.assign(project, $event)"
@select="select" @select="select"
@search="findLists" @search="findProjects"
> >
<template #searchResult="{option}"> <template #searchResult="{option}">
<span class="list-namespace-title search-result">{{ namespace((option as IList).namespaceId) }} ></span> <span class="project-namespace-title search-result">{{ namespace((option as IProject).namespaceId) }} ></span>
{{ (option as IList).title }} {{ (option as IProject).title }}
</template> </template>
</Multiselect> </Multiselect>
</template> </template>
@ -22,19 +22,19 @@ import {reactive, ref, watch} from 'vue'
import type {PropType} from 'vue' import type {PropType} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import {useListStore} from '@/stores/lists' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object as PropType<IList>, type: Object as PropType<IProject>,
required: false, required: false,
}, },
}) })
@ -42,45 +42,45 @@ const emit = defineEmits(['update:modelValue'])
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const list: IList = reactive(new ListModel()) const project: IProject = reactive(new ProjectModel())
watch( watch(
() => props.modelValue, () => props.modelValue,
(newList) => Object.assign(list, newList), (newProject) => Object.assign(project, newProject),
{ {
immediate: true, immediate: true,
deep: true, deep: true,
}, },
) )
const listStore = useListStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const foundLists = ref<IList[]>([]) const foundProjects = ref<IProject[]>([])
function findLists(query: string) { function findProjects(query: string) {
if (query === '') { if (query === '') {
select(null) select(null)
} }
foundLists.value = listStore.searchList(query) foundProjects.value = projectStore.searchProject(query)
} }
function select(l: IList | null) { function select(l: IProject | null) {
if (l === null) { if (l === null) {
return return
} }
Object.assign(list, l) Object.assign(project, l)
emit('update:modelValue', list) emit('update:modelValue', project)
} }
function namespace(namespaceId: INamespace['id']) { function namespace(namespaceId: INamespace['id']) {
const namespace = namespaceStore.getNamespaceById(namespaceId) const namespace = namespaceStore.getNamespaceById(namespaceId)
return namespace !== null return namespace !== null
? namespace.title ? namespace.title
: t('list.shared') : t('project.shared')
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.list-namespace-title { .project-namespace-title {
color: var(--grey-500); color: var(--grey-500);
} }
</style> </style>

View File

@ -37,14 +37,14 @@
{{ $t('task.quickAddMagic.multiple') }} {{ $t('task.quickAddMagic.multiple') }}
</p> </p>
<h3>{{ $t('list.list.title') }}</h3> <h3>{{ $t('project.list.title') }}</h3>
<p> <p>
{{ $t('task.quickAddMagic.list1', {prefix: prefixes.list}) }} {{ $t('task.quickAddMagic.project1', {prefix: prefixes.project}) }}
{{ $t('task.quickAddMagic.list2') }} {{ $t('task.quickAddMagic.project2') }}
</p> </p>
<p> <p>
{{ $t('task.quickAddMagic.list3') }} {{ $t('task.quickAddMagic.project3') }}
{{ $t('task.quickAddMagic.list4', {prefix: prefixes.list}) }} {{ $t('task.quickAddMagic.project4', {prefix: prefixes.project}) }}
</p> </p>
<h3>{{ $t('task.quickAddMagic.dateAndTime') }}</h3> <h3>{{ $t('task.quickAddMagic.dateAndTime') }}</h3>

View File

@ -43,8 +43,8 @@
:class="{'is-strikethrough': task.done}" :class="{'is-strikethrough': task.done}"
> >
<span <span
class="different-list" class="different-project"
v-if="task.listId !== listId" v-if="task.projectId !== projectId"
> >
<span <span
v-if="task.differentNamespace !== null" v-if="task.differentNamespace !== null"
@ -52,9 +52,9 @@
{{ task.differentNamespace }} > {{ task.differentNamespace }} >
</span> </span>
<span <span
v-if="task.differentList !== null" v-if="task.differentProject !== null"
v-tooltip="$t('task.relation.differentList')"> v-tooltip="$t('task.relation.differentProject')">
{{ task.differentList }} > {{ task.differentProject }} >
</span> </span>
</span> </span>
{{ task.title }} {{ task.title }}
@ -98,8 +98,8 @@
:class="{ 'is-strikethrough': t.done}" :class="{ 'is-strikethrough': t.done}"
> >
<span <span
class="different-list" class="different-project"
v-if="t.listId !== listId" v-if="t.projectId !== projectId"
> >
<span <span
v-if="t.differentNamespace !== null" v-if="t.differentNamespace !== null"
@ -107,9 +107,9 @@
{{ t.differentNamespace }} > {{ t.differentNamespace }} >
</span> </span>
<span <span
v-if="t.differentList !== null" v-if="t.differentProject !== null"
v-tooltip="$t('task.relation.differentList')"> v-tooltip="$t('task.relation.differentProject')">
{{ t.differentList }} > {{ t.differentProject }} >
</span> </span>
</span> </span>
{{ t.title }} {{ t.title }}
@ -186,7 +186,7 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
listId: { projectId: {
type: Number, type: Number,
default: 0, default: 0,
}, },
@ -230,17 +230,17 @@ async function findTasks(newQuery: string) {
foundTasks.value = await taskService.getAll({}, {s: newQuery}) foundTasks.value = await taskService.getAll({}, {s: newQuery})
} }
const getListAndNamespaceById = (listId: number) => namespaceStore.getListAndNamespaceById(listId, true) const getProjectAndNamespaceById = (projectId: number) => namespaceStore.getProjectAndNamespaceById(projectId, true)
const namespace = computed(() => getListAndNamespaceById(props.listId)?.namespace) const namespace = computed(() => getProjectAndNamespaceById(props.projectId)?.namespace)
function mapRelatedTasks(tasks: ITask[]) { function mapRelatedTasks(tasks: ITask[]) {
return tasks.map(task => { return tasks.map(task => {
// by doing this here once we can save a lot of duplicate calls in the template // by doing this here once we can save a lot of duplicate calls in the template
const { const {
list, project,
namespace: taskNamespace, namespace: taskNamespace,
} = getListAndNamespaceById(task.listId) || {list: null, namespace: null} } = getProjectAndNamespaceById(task.projectId) || {project: null, namespace: null}
return { return {
...task, ...task,
@ -248,10 +248,10 @@ function mapRelatedTasks(tasks: ITask[]) {
(taskNamespace !== null && (taskNamespace !== null &&
taskNamespace.id !== namespace.value.id && taskNamespace.id !== namespace.value.id &&
taskNamespace?.title) || null, taskNamespace?.title) || null,
differentList: differentProject:
(list !== null && (project !== null &&
task.listId !== props.listId && task.projectId !== props.projectId &&
list?.title) || null, project?.title) || null,
} }
}) })
} }
@ -343,7 +343,7 @@ async function removeTaskRelation() {
} }
async function createAndRelateTask(title: string) { async function createAndRelateTask(title: string) {
const newTask = await taskService.create(new TaskModel({title, listId: props.listId})) const newTask = await taskService.create(new TaskModel({title, projectId: props.projectId}))
newTaskRelation.task = newTask newTaskRelation.task = newTask
await addTaskRelation() await addTaskRelation()
} }
@ -351,7 +351,7 @@ async function createAndRelateTask(title: string) {
async function toggleTaskDone(task: ITask) { async function toggleTaskDone(task: ITask) {
await taskStore.update(task) await taskStore.update(task)
// Find the task in the list and update it so that it is correctly strike through // Find the task in the project and update it so that it is correctly strike through
Object.entries(relatedTasks.value).some(([kind, tasks]) => { Object.entries(relatedTasks.value).some(([kind, tasks]) => {
return (tasks as ITask[]).some((t, key) => { return (tasks as ITask[]).some((t, key) => {
const found = t.id === task.id const found = t.id === task.id
@ -379,7 +379,7 @@ async function toggleTaskDone(task: ITask) {
} }
} }
.different-list { .different-project {
color: var(--grey-500); color: var(--grey-500);
width: auto; width: auto;
} }

View File

@ -11,23 +11,23 @@
/> />
<ColorBubble <ColorBubble
v-if="showListColor && listColor !== '' && currentList.id !== task.listId" v-if="showProjectColor && projectColor !== '' && currentProject.id !== task.projectId"
:color="listColor" :color="projectColor"
class="mr-1" class="mr-1"
/> />
<div <div
:class="{ 'done': task.done, 'show-list': showList && taskList !== null}" :class="{ 'done': task.done, 'show-project': showProject && project !== null}"
class="tasktext" class="tasktext"
> >
<span> <span>
<router-link <router-link
v-if="showList && taskList !== null" v-if="showProject && project !== null"
:to="{ name: 'list.list', params: { listId: task.listId } }" :to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-list" class="task-project"
:class="{'mr-2': task.hexColor !== ''}" :class="{'mr-2': task.hexColor !== ''}"
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})"> v-tooltip="$t('task.detail.belongsToProject', {project: project.title})">
{{ taskList.title }} {{ project.title }}
</router-link> </router-link>
<ColorBubble <ColorBubble
@ -84,13 +84,13 @@
<priority-label :priority="task.priority" :done="task.done"/> <priority-label :priority="task.priority" :done="task.done"/>
<span> <span>
<span class="list-task-icon" v-if="task.attachments.length > 0"> <span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/> <icon icon="paperclip"/>
</span> </span>
<span class="list-task-icon" v-if="task.description"> <span class="project-task-icon" v-if="task.description">
<icon icon="align-left"/> <icon icon="align-left"/>
</span> </span>
<span class="list-task-icon" v-if="task.repeatAfter.amount > 0"> <span class="project-task-icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/> <icon icon="history"/>
</span> </span>
</span> </span>
@ -107,12 +107,12 @@
</progress> </progress>
<router-link <router-link
v-if="!showList && currentList.id !== task.listId && taskList !== null" v-if="!showProject && currentProject.id !== task.projectId && project !== null"
:to="{ name: 'list.list', params: { listId: task.listId } }" :to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-list" class="task-project"
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})" v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
> >
{{ taskList.title }} {{ project.title }}
</router-link> </router-link>
<BaseButton <BaseButton
@ -151,7 +151,7 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate' import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import {success} from '@/message' import {success} from '@/message'
import {useListStore} from '@/stores/lists' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
@ -165,7 +165,7 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showList: { showProject: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
@ -173,7 +173,7 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showListColor: { showProjectColor: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
@ -210,18 +210,18 @@ onBeforeUnmount(() => {
}) })
const baseStore = useBaseStore() const baseStore = useBaseStore()
const listStore = useListStore() const projectStore = useProjectStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const taskList = computed(() => listStore.getListById(task.value.listId)) const project = computed(() => projectStore.getProjectById(task.value.projectId))
const listColor = computed(() => taskList.value !== null ? taskList.value.hexColor : '') const projectColor = computed(() => project.value !== null ? project.value.hexColor : '')
const currentList = computed(() => { const currentProject = computed(() => {
return typeof baseStore.currentList === 'undefined' ? { return typeof baseStore.currentProject === 'undefined' ? {
id: 0, id: 0,
title: '', title: '',
} : baseStore.currentList } : baseStore.currentProject
}) })
const taskDetailRoute = computed(() => ({ const taskDetailRoute = computed(() => ({
@ -314,7 +314,7 @@ function hideDeferDueDatePopup(e) {
} }
} }
.task-list { .task-project {
width: auto; width: auto;
color: var(--grey-400); color: var(--grey-400);
font-size: .9rem; font-size: .9rem;
@ -329,7 +329,7 @@ function hideDeferDueDatePopup(e) {
width: 27px; width: 27px;
} }
.list-task-icon { .project-task-icon {
margin-left: 6px; margin-left: 6px;
&:not(:first-of-type) { &:not(:first-of-type) {
@ -394,7 +394,7 @@ function hideDeferDueDatePopup(e) {
width: auto; width: auto;
} }
.show-list .parent-tasks { .show-project .parent-tasks {
padding-left: .25rem; padding-left: .25rem;
} }

View File

@ -58,9 +58,9 @@ const SORT_BY_DEFAULT: SortBy = {
} }
/** /**
* This mixin provides a base set of methods and properties to get tasks on a list. * This mixin provides a base set of methods and properties to get tasks.
*/ */
export function useTaskList(listId, sortByDefault: SortBy = SORT_BY_DEFAULT) { export function useTaskList(projectId, sortByDefault: SortBy = SORT_BY_DEFAULT) {
const params = ref({...getDefaultParams()}) const params = ref({...getDefaultParams()})
const search = ref('') const search = ref('')
@ -80,7 +80,7 @@ export function useTaskList(listId, sortByDefault: SortBy = SORT_BY_DEFAULT) {
loadParams = formatSortOrder(sortBy.value, loadParams) loadParams = formatSortOrder(sortBy.value, loadParams)
return [ return [
{listId: listId.value}, {projectId: projectId.value},
loadParams, loadParams,
page.value || 1, page.value || 1,
] ]

View File

@ -3,7 +3,7 @@ import type {INamespace} from '@/modelTypes/INamespace'
export const getNamespaceTitle = (n: INamespace) => { export const getNamespaceTitle = (n: INamespace) => {
if (n.id === -1) { if (n.id === -1) {
return i18n.global.t('namespace.pseudo.sharedLists.title') return i18n.global.t('namespace.pseudo.sharedProjects.title')
} }
if (n.id === -2) { if (n.id === -2) {
return i18n.global.t('namespace.pseudo.favorites.title') return i18n.global.t('namespace.pseudo.favorites.title')

View File

@ -1,9 +1,9 @@
import {i18n} from '@/i18n' import {i18n} from '@/i18n'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
export function getListTitle(l: IList) { export function getProjectTitle(l: IProject) {
if (l.id === -1) { if (l.id === -1) {
return i18n.global.t('list.pseudo.favorites.title') return i18n.global.t('project.pseudo.favorites.title')
} }
return l.title return l.title
} }

View File

@ -1,4 +1,4 @@
import {getListFromPrefix} from '@/modules/parseTaskText' import {getProjectFromPrefix} from '@/modules/parseTaskText'
export interface TaskWithParent { export interface TaskWithParent {
title: string, title: string,
@ -26,7 +26,7 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
list: null, list: null,
} }
task.list = getListFromPrefix(task.title) task.list = getProjectFromPrefix(task.title)
if (index === 0) { if (index === 0) {
return task return task
@ -49,7 +49,7 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
task.parent = task.parent.replace(spaceRegex, '') task.parent = task.parent.replace(spaceRegex, '')
if (task.list === null) { if (task.list === null) {
// This allows to specify a list once for the parent task and inherit it to all subtasks // This allows to specify a list once for the parent task and inherit it to all subtasks
task.list = getListFromPrefix(task.parent) task.list = getProjectFromPrefix(task.parent)
} }
} }

View File

@ -1,5 +1,5 @@
import type {IBucket} from '@/modelTypes/IBucket' import type {IBucket} from '@/modelTypes/IBucket'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
const key = 'collapsedBuckets' const key = 'collapsedBuckets'
@ -13,22 +13,22 @@ function getAllState() {
} }
export const saveCollapsedBucketState = ( export const saveCollapsedBucketState = (
listId: IList['id'], projectId: IProject['id'],
collapsedBuckets: CollapsedBuckets, collapsedBuckets: CollapsedBuckets,
) => { ) => {
const state = getAllState() const state = getAllState()
state[listId] = collapsedBuckets state[projectId] = collapsedBuckets
for (const bucketId in state[listId]) { for (const bucketId in state[projectId]) {
if (!state[listId][bucketId]) { if (!state[projectId][bucketId]) {
delete state[listId][bucketId] delete state[projectId][bucketId]
} }
} }
localStorage.setItem(key, JSON.stringify(state)) localStorage.setItem(key, JSON.stringify(state))
} }
export function getCollapsedBucketState(listId : IList['id']) { export function getCollapsedBucketState(projectId : IProject['id']) {
const state = getAllState() const state = getAllState()
return typeof state[listId] !== 'undefined' return typeof state[projectId] !== 'undefined'
? state[listId] ? state[projectId]
: {} : {}
} }

View File

@ -1,53 +1,63 @@
import type { IList } from '@/modelTypes/IList' import type { IProject } from '@/modelTypes/IProject'
type ListView = Record<IList['id'], string> type ProjectView = Record<IProject['id'], string>
const DEFAULT_LIST_VIEW = 'list.list' as const const DEFAULT_PROJECT_VIEW = 'project.list' as const
const PROJECT_VIEW_SETTINGS_KEY = 'projectView'
/** /**
* Save the current list view to local storage * Save the current project view to local storage
*/ */
export function saveListView(listId: IList['id'], routeName: string) { export function saveProjectView(projectId: IProject['id'], routeName: string) {
if (routeName.includes('settings.')) { if (routeName.includes('settings.')) {
return return
} }
if (!listId) { if (!projectId) {
return return
} }
// We use local storage and not the store here to make it persistent across reloads. // We use local storage and not the store here to make it persistent across reloads.
const savedListView = localStorage.getItem('listView') const savedProjectView = localStorage.getItem(PROJECT_VIEW_SETTINGS_KEY)
let savedListViewJson: ListView | false = false let savedProjectViewJson: ProjectView | false = false
if (savedListView !== null) { if (savedProjectView !== null) {
savedListViewJson = JSON.parse(savedListView) as ListView savedProjectViewJson = JSON.parse(savedProjectView) as ProjectView
} }
let listView: ListView = {} let projectView: ProjectView = {}
if (savedListViewJson) { if (savedProjectViewJson) {
listView = savedListViewJson projectView = savedProjectViewJson
} }
listView[listId] = routeName projectView[projectId] = routeName
localStorage.setItem('listView', JSON.stringify(listView)) localStorage.setItem(PROJECT_VIEW_SETTINGS_KEY, JSON.stringify(projectView))
} }
export const getListView = (listId: IList['id']) => { export const getProjectView = (projectId: IProject['id']) => {
// Remove old stored settings // Migrate old setting over
const savedListView = localStorage.getItem('listView') // TODO: remove when 1.0 release
if (savedListView !== null && savedListView.startsWith('list.')) { const oldListViewSettings = localStorage.getItem('listView')
if (oldListViewSettings !== null) {
localStorage.setItem(PROJECT_VIEW_SETTINGS_KEY, oldListViewSettings)
localStorage.removeItem('listView') localStorage.removeItem('listView')
} }
if (!savedListView) { // Remove old stored settings
return DEFAULT_LIST_VIEW // TODO: remove when 1.0 release
const savedProjectView = localStorage.getItem(PROJECT_VIEW_SETTINGS_KEY)
if (savedProjectView !== null && savedProjectView.startsWith('project.')) {
localStorage.removeItem(PROJECT_VIEW_SETTINGS_KEY)
} }
const savedListViewJson: ListView = JSON.parse(savedListView) if (!savedProjectView) {
return DEFAULT_PROJECT_VIEW
if (!savedListViewJson[listId]) {
return DEFAULT_LIST_VIEW
} }
return savedListViewJson[listId] const savedProjectViewJson: ProjectView = JSON.parse(savedProjectView)
if (!savedProjectViewJson[projectId]) {
return DEFAULT_PROJECT_VIEW
}
return savedProjectViewJson[projectId]
} }

View File

@ -5,10 +5,10 @@
"welcomeDay": "Hi {username}!", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "project": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new project for your new tasks:",
"new": "New list", "new": "New project",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
}, },
@ -85,7 +85,7 @@
"weekStartSunday": "Sunday", "weekStartSunday": "Sunday",
"weekStartMonday": "Monday", "weekStartMonday": "Monday",
"language": "Language", "language": "Language",
"defaultList": "Default List", "defaultProject": "Default Project",
"timezone": "Time Zone", "timezone": "Time Zone",
"overdueTasksRemindersTime": "Overdue tasks reminder email time" "overdueTasksRemindersTime": "Overdue tasks reminder email time"
}, },
@ -143,7 +143,7 @@
}, },
"deletion": { "deletion": {
"title": "Delete your Vikunja Account", "title": "Delete your Vikunja Account",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, lists, tasks and everything associated with it.", "text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.",
"text2": "To proceed, please enter your password. You will receive an email with further instructions.", "text2": "To proceed, please enter your password. You will receive an email with further instructions.",
"confirm": "Delete my account", "confirm": "Delete my account",
"requestSuccess": "The request was successful. You'll receive an email with further instructions.", "requestSuccess": "The request was successful. You'll receive an email with further instructions.",
@ -157,39 +157,39 @@
}, },
"export": { "export": {
"title": "Export your Vikunja data", "title": "Export your Vikunja data",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Lists, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", "description": "You can request a copy of all your Vikunja data. This include Namespaces, Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"descriptionPasswordRequired": "Please enter your password to proceed:", "descriptionPasswordRequired": "Please enter your password to proceed:",
"request": "Request a copy of my Vikunja Data", "request": "Request a copy of my Vikunja Data",
"success": "You've successfully requested your Vikunja Data! We will send you an email once it's ready to download.", "success": "You've successfully requested your Vikunja Data! We will send you an email once it's ready to download.",
"downloadTitle": "Download your exported Vikunja data" "downloadTitle": "Download your exported Vikunja data"
} }
}, },
"list": { "project": {
"archived": "This list is archived. It is not possible to create new or edit tasks for it.", "archived": "This project is archived. It is not possible to create new or edit tasks for it.",
"title": "List Title", "title": "Project Title",
"color": "Color", "color": "Color",
"lists": "Lists", "projects": "Projects",
"search": "Type to search for a list…", "search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this project",
"shared": "Shared Lists", "shared": "Shared Projects",
"noDescriptionAvailable": "No list description is available.", "noDescriptionAvailable": "No project description is available.",
"create": { "create": {
"header": "New list", "header": "New project",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The project's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The project was successfully created.",
"addListRequired": "Please specify a list or set a default list in the settings." "addProjectRequired": "Please specify a project or set a default project in the settings."
}, },
"archive": { "archive": {
"title": "Archive \"{list}\"", "title": "Archive \"{project}\"",
"archive": "Archive this list", "archive": "Archive this project",
"unarchive": "Un-Archive this list", "unarchive": "Un-Archive this project",
"unarchiveText": "You will be able to create new tasks or edit it.", "unarchiveText": "You will be able to create new tasks or edit it.",
"archiveText": "You won't be able to edit this list or create new tasks until you un-archive it.", "archiveText": "You won't be able to edit this project or create new tasks until you un-archive it.",
"success": "The list was successfully archived." "success": "The project was successfully archived."
}, },
"background": { "background": {
"title": "Set list background", "title": "Set project background",
"remove": "Remove Background", "remove": "Remove Background",
"upload": "Choose a background from your pc", "upload": "Choose a background from your pc",
"searchPlaceholder": "Search for a background…", "searchPlaceholder": "Search for a background…",
@ -199,40 +199,40 @@
"removeSuccess": "The background has been removed successfully!" "removeSuccess": "The background has been removed successfully!"
}, },
"delete": { "delete": {
"title": "Delete \"{list}\"", "title": "Delete \"{project}\"",
"header": "Delete this list", "header": "Delete this project",
"text1": "Are you sure you want to delete this list and all of its contents?", "text1": "Are you sure you want to delete this project and all of its contents?",
"text2": "This includes all tasks and CANNOT BE UNDONE!", "text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The list was successfully deleted.", "success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.", "tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"noTasksToDelete": "This list does not contain any tasks, it should be safe to delete." "noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
}, },
"duplicate": { "duplicate": {
"title": "Duplicate this list", "title": "Duplicate this project",
"label": "Duplicate", "label": "Duplicate",
"text": "Select a namespace which should hold the duplicated list:", "text": "Select a namespace which should hold the duplicated project:",
"success": "The list was successfully duplicated." "success": "The project was successfully duplicated."
}, },
"edit": { "edit": {
"header": "Edit This List", "header": "Edit This Project",
"title": "Edit \"{list}\"", "title": "Edit \"{project}\"",
"titlePlaceholder": "The list title goes here…", "titlePlaceholder": "The project title goes here…",
"identifierTooltip": "The list identifier can be used to uniquely identify a task across lists. You can set it to empty to disable it.", "identifierTooltip": "The project identifier can be used to uniquely identify a task across projects. You can set it to empty to disable it.",
"identifier": "List Identifier", "identifier": "Project Identifier",
"identifierPlaceholder": "The list identifier goes here…", "identifierPlaceholder": "The project identifier goes here…",
"description": "Description", "description": "Description",
"descriptionPlaceholder": "The lists description goes here…", "descriptionPlaceholder": "The projects description goes here…",
"color": "Color", "color": "Color",
"success": "The list was successfully updated." "success": "The project was successfully updated."
}, },
"share": { "share": {
"header": "Share this list", "header": "Share this project",
"title": "Share \"{list}\"", "title": "Share \"{project}\"",
"share": "Share", "share": "Share",
"links": { "links": {
"title": "Share Links", "title": "Share Links",
"what": "What is a share link?", "what": "What is a share link?",
"explanation": "Share Links allow you to easily share a list with other users who don't have an account on Vikunja.", "explanation": "Share Links allow you to easily share a project with other users who don't have an account on Vikunja.",
"create": "Create a new link share", "create": "Create a new link share",
"name": "Name (optional)", "name": "Name (optional)",
"namePlaceholder": "e.g. Lorem Ipsum", "namePlaceholder": "e.g. Lorem Ipsum",
@ -241,7 +241,7 @@
"passwordExplanation": "When authenticating, the user will be required to enter this password.", "passwordExplanation": "When authenticating, the user will be required to enter this password.",
"noName": "No name set", "noName": "No name set",
"remove": "Remove a link share", "remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this list with this link share. This cannot be undone!", "removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
"createSuccess": "The link share was successfully created.", "createSuccess": "The link share was successfully created.",
"deleteSuccess": "The link share was successfully deleted", "deleteSuccess": "The link share was successfully deleted",
"view": "View", "view": "View",
@ -274,7 +274,7 @@
"title": "List", "title": "List",
"add": "Add", "add": "Add",
"addPlaceholder": "Add a new task…", "addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.", "empty": "This project is currently empty.",
"newTaskCta": "Create a new task.", "newTaskCta": "Create a new task.",
"editTask": "Edit Task" "editTask": "Edit Task"
}, },
@ -322,36 +322,36 @@
} }
}, },
"namespace": { "namespace": {
"title": "Namespaces & Lists", "title": "Namespaces & Projects",
"namespace": "Namespace", "namespace": "Namespace",
"showArchived": "Show Archived", "showArchived": "Show Archived",
"noneAvailable": "You don't have any namespaces right now.", "noneAvailable": "You don't have any namespaces right now.",
"unarchive": "Un-Archive", "unarchive": "Un-Archive",
"archived": "Archived", "archived": "Archived",
"noLists": "This namespace does not contain any lists.", "noProjects": "This namespace does not contain any projects.",
"createList": "Create a new list in this namespace.", "createProject": "Create a new project in this namespace.",
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "New namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namespace.", "explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
"success": "The namespace was successfully created." "success": "The namespace was successfully created."
}, },
"archive": { "archive": {
"titleArchive": "Archive \"{namespace}\"", "titleArchive": "Archive \"{namespace}\"",
"titleUnarchive": "Un-Archive \"{namespace}\"", "titleUnarchive": "Un-Archive \"{namespace}\"",
"archiveText": "You won't be able to edit this namespace or create new lists until you un-archive it. This will also archive all lists in this namespace.", "archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
"unarchiveText": "You will be able to create new lists or edit it.", "unarchiveText": "You will be able to create new projects or edit it.",
"success": "The namespace was successfully archived.", "success": "The namespace was successfully archived.",
"unarchiveSuccess": "The namespace was successfully un-archived.", "unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new lists or edit it." "description": "If a namespace is archived, you cannot create new projects or edit it."
}, },
"delete": { "delete": {
"title": "Delete \"{namespace}\"", "title": "Delete \"{namespace}\"",
"text1": "Are you sure you want to delete this namespace and all of its contents?", "text1": "Are you sure you want to delete this namespace and all of its contents?",
"text2": "This includes all lists and tasks and CANNOT BE UNDONE!", "text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
"success": "The namespace was successfully deleted." "success": "The namespace was successfully deleted."
}, },
"edit": { "edit": {
@ -371,8 +371,8 @@
"isArchived": "This namespace is archived" "isArchived": "This namespace is archived"
}, },
"pseudo": { "pseudo": {
"sharedLists": { "sharedProjects": {
"title": "Shared Lists" "title": "Shared Projects"
}, },
"favorites": { "favorites": {
"title": "Favorites" "title": "Favorites"
@ -403,7 +403,7 @@
}, },
"create": { "create": {
"title": "New Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter", "action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter." "titleRequired": "Please provide a title for the filter."
}, },
@ -435,7 +435,7 @@
"label": { "label": {
"title": "Labels", "title": "Labels",
"manage": "Manage labels", "manage": "Manage labels",
"description": "Click on a label to edit it. You can edit all labels you created, you can use all labels which are associated with a task to whose list you have access.", "description": "Click on a label to edit it. You can edit all labels you created, you can use all labels which are associated with a task to whose project you have access.",
"newCTA": "You currently do not have any labels.", "newCTA": "You currently do not have any labels.",
"search": "Type to search for a label…", "search": "Type to search for a label…",
"create": { "create": {
@ -460,7 +460,7 @@
}, },
"sharing": { "sharing": {
"authenticating": "Authenticating…", "authenticating": "Authenticating…",
"passwordRequired": "This shared list requires a password. Please enter it below:", "passwordRequired": "This shared project requires a password. Please enter it below:",
"error": "An error occured.", "error": "An error occured.",
"invalidPassword": "The password is invalid." "invalidPassword": "The password is invalid."
}, },
@ -529,7 +529,7 @@
"code": "Code", "code": "Code",
"quote": "Quote", "quote": "Quote",
"unorderedList": "Unordered List", "unorderedList": "Unordered List",
"orderedList": "Ordered List", "orderedList ": "Ordered List",
"cleanBlock": "Clean Block", "cleanBlock": "Clean Block",
"link": "Link", "link": "Link",
"image": "Image", "image": "Image",
@ -622,7 +622,7 @@
"chooseDueDate": "Click here to set a due date", "chooseDueDate": "Click here to set a due date",
"chooseStartDate": "Click here to set a start date", "chooseStartDate": "Click here to set a start date",
"chooseEndDate": "Click here to set an end date", "chooseEndDate": "Click here to set an end date",
"move": "Move task to a different list", "move": "Move task to a different project",
"done": "Mark task done!", "done": "Mark task done!",
"undone": "Mark as undone", "undone": "Mark as undone",
"created": "Created {0} by {1}", "created": "Created {0} by {1}",
@ -630,7 +630,7 @@
"doneAt": "Done {0}", "doneAt": "Done {0}",
"updateSuccess": "The task was saved successfully.", "updateSuccess": "The task was saved successfully.",
"deleteSuccess": "The task has been deleted successfully.", "deleteSuccess": "The task has been deleted successfully.",
"belongsToList": "This task belongs to list '{list}'", "belongsToProject": "This task belongs to project '{project}'",
"due": "Due {at}", "due": "Due {at}",
"closePopup": "Close popup", "closePopup": "Close popup",
"delete": { "delete": {
@ -650,7 +650,7 @@
"percentDone": "Set Progress", "percentDone": "Set Progress",
"attachments": "Add Attachments", "attachments": "Add Attachments",
"relatedTasks": "Add Relation", "relatedTasks": "Add Relation",
"moveList": "Move", "moveProject": "Move",
"color": "Set Color", "color": "Set Color",
"delete": "Delete", "delete": "Delete",
"favorite": "Add to Favorites", "favorite": "Add to Favorites",
@ -677,21 +677,21 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.", "subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.", "subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.", "subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.", "notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.", "subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.", "notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.", "subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.", "notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccessNamespace": "You are now subscribed to this namespace", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace", "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list", "subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessList": "You are now unsubscribed to this list", "unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "You are now subscribed to this task", "subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task" "unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
@ -765,7 +765,7 @@
"new": "New Task Relation", "new": "New Task Relation",
"searchPlaceholder": "Type search for a new task to add as related…", "searchPlaceholder": "Type search for a new task to add as related…",
"createPlaceholder": "Add this as new related task", "createPlaceholder": "Add this as new related task",
"differentList": "This task belongs to a different list.", "differentProject": "This task belongs to a different project.",
"differentNamespace": "This task belongs to a different namespace.", "differentNamespace": "This task belongs to a different namespace.",
"noneYet": "No task relations yet.", "noneYet": "No task relations yet.",
"delete": "Delete Task Relation", "delete": "Delete Task Relation",
@ -815,10 +815,10 @@
"priority1": "To set a task's priority, add a number 1-5, prefixed with a {prefix}.", "priority1": "To set a task's priority, add a number 1-5, prefixed with a {prefix}.",
"priority2": "The higher the number, the higher the priority.", "priority2": "The higher the number, the higher the priority.",
"assignees": "To directly assign the task to a user, add their username prefixed with {prefix} to the task.", "assignees": "To directly assign the task to a user, add their username prefixed with {prefix} to the task.",
"list1": "To set a list for the task to appear in, enter its name prefixed with {prefix}.", "project1": "To set a project for the task to appear in, enter its name prefixed with {prefix}.",
"list2": "This will return an error if the list does not exist.", "project2": "This will return an error if the project does not exist.",
"list3": "To use spaces, simply add a \" or ' around the list name.", "project3": "To use spaces, simply add a \" or ' around the project name.",
"list4": "For example: {prefix}\"List with spaces\".", "project4": "For example: {prefix}\"Project with spaces\".",
"dateAndTime": "Date and time", "dateAndTime": "Date and time",
"date": "Any date will be used as the due date of the new task. You can use dates in any of these formats:", "date": "Any date will be used as the due date of the new task. You can use dates in any of these formats:",
"dateWeekday": "any weekday, will use the next date with that date", "dateWeekday": "any weekday, will use the next date with that date",
@ -851,19 +851,19 @@
"delete": { "delete": {
"header": "Delete the team", "header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?", "text1": "Are you sure you want to delete this team and all of its members?",
"text2": "All team members will lose access to lists and namespaces shared with this team. This CANNOT BE UNDONE!", "text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!",
"success": "The team was successfully deleted." "success": "The team was successfully deleted."
}, },
"deleteUser": { "deleteUser": {
"header": "Remove a user from the team", "header": "Remove a user from the team",
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
}, },
"leave": { "leave": {
"title": "Leave team", "title": "Leave team",
"text1": "Are you sure you want to leave this team?", "text1": "Are you sure you want to leave this team?",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.", "text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team." "success": "You have successfully left the team."
} }
}, },
@ -895,13 +895,13 @@
"attachment": "Add an attachment to this task", "attachment": "Add an attachment to this task",
"related": "Modify related tasks of this task", "related": "Modify related tasks of this task",
"color": "Change the color of this task", "color": "Change the color of this task",
"move": "Move this task to another list", "move": "Move this task to another project",
"reminder": "Manage reminders of this task", "reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description" "description": "Toggle editing of the task description"
}, },
"list": { "project": {
"title": "List Views", "title": "Project Views",
"switchToListView": "Switch to list view", "switchToListView": "Switch to project view",
"switchToGanttView": "Switch to gantt view", "switchToGanttView": "Switch to gantt view",
"switchToKanbanView": "Switch to kanban view", "switchToKanbanView": "Switch to kanban view",
"switchToTableView": "Switch to table view" "switchToTableView": "Switch to table view"
@ -910,7 +910,7 @@
"title": "Navigation", "title": "Navigation",
"overview": "Navigate to overview", "overview": "Navigate to overview",
"upcoming": "Navigate to upcoming tasks", "upcoming": "Navigate to upcoming tasks",
"namespaces": "Navigate to namespaces & lists", "namespaces": "Navigate to namespaces & projects",
"labels": "Navigate to labels", "labels": "Navigate to labels",
"teams": "Navigate to teams" "teams": "Navigate to teams"
} }
@ -927,7 +927,7 @@
"unarchive": "Un-Archive", "unarchive": "Un-Archive",
"setBackground": "Set background", "setBackground": "Set background",
"share": "Share", "share": "Share",
"newList": "New list" "newProject": "New project"
}, },
"apiConfig": { "apiConfig": {
"url": "Vikunja URL", "url": "Vikunja URL",
@ -946,24 +946,24 @@
"notification": { "notification": {
"title": "Notifications", "title": "Notifications",
"none": "You don't have any notifications. Have a nice day!", "none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions on namespaces, lists or tasks you subscribed to happen." "explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen."
}, },
"quickActions": { "quickActions": {
"commands": "Commands", "commands": "Commands",
"placeholder": "Type a command or search…", "placeholder": "Type a command or search…",
"hint": "You can use {list} to limit the search to a list. Combine {list} or {label} (labels) with a search query to search for a task with these labels or on that list. Use {assignee} to only search for teams.", "hint": "You can use {project} to limit the search to a project. Combine {project} or {label} (labels) with a search query to search for a task with these labels or on that project. Use {assignee} to only search for teams.",
"tasks": "Tasks", "tasks": "Tasks",
"lists": "Lists", "projects": "Projects",
"teams": "Teams", "teams": "Teams",
"newList": "Enter the title of the new list…", "newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…", "newTask": "Enter the title of the new task…",
"newNamespace": "Enter the title of the new namespace…", "newNamespace": "Enter the title of the new namespace…",
"newTeam": "Enter the name of the new team…", "newTeam": "Enter the name of the new team…",
"createTask": "Create a task in the current list ({title})", "createTask": "Create a task in the current project ({title})",
"createList": "Create a list in the current namespace ({title})", "createProject": "Create a project in the current namespace ({title})",
"cmds": { "cmds": {
"newTask": "New task", "newTask": "New task",
"newList": "New list", "newProject": "New project",
"newNamespace": "New namespace", "newNamespace": "New namespace",
"newTeam": "New team" "newTeam": "New team"
} }
@ -995,15 +995,15 @@
"1018": "The user avatar type setting is invalid.", "1018": "The user avatar type setting is invalid.",
"2001": "ID cannot be empty or 0.", "2001": "ID cannot be empty or 0.",
"2002": "Some of the request data was invalid.", "2002": "Some of the request data was invalid.",
"3001": "The list does not exist.", "3001": "The project does not exist.",
"3004": "You need to have read permissions on that list to perform that action.", "3004": "You need to have read permissions on that project to perform that action.",
"3005": "The list title cannot be empty.", "3005": "The project title cannot be empty.",
"3006": "The list share does not exist.", "3006": "The project share does not exist.",
"3007": "A list with this identifier already exists.", "3007": "A project with this identifier already exists.",
"3008": "The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list.", "3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The list task text cannot be empty.", "4001": "The project task text cannot be empty.",
"4002": "The list task does not exist.", "4002": "The project task does not exist.",
"4003": "All bulk editing tasks must belong to the same list.", "4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.", "4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.", "4005": "You do not have the right to see the task.",
"4006": "You can't set a parent task as the task itself.", "4006": "You can't set a parent task as the task itself.",
@ -1029,21 +1029,21 @@
"5012": "The namespace is archived and can therefore only be accessed read only.", "5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be empty.", "6001": "The team name cannot be empty.",
"6002": "The team does not exist.", "6002": "The team does not exist.",
"6004": "The team already has access to that namespace or list.", "6004": "The team already has access to that namespace or project.",
"6005": "The user is already a member of that team.", "6005": "The user is already a member of that team.",
"6006": "Cannot delete the last team member.", "6006": "Cannot delete the last team member.",
"6007": "The team does not have access to the list to perform that action.", "6007": "The team does not have access to the project to perform that action.",
"7002": "The user already has access to that list.", "7002": "The user already has access to that project.",
"7003": "You do not have access to that list.", "7003": "You do not have access to that project.",
"8001": "This label already exists on that task.", "8001": "This label already exists on that task.",
"8002": "The label does not exist.", "8002": "The label does not exist.",
"8003": "You do not have access to this label.", "8003": "You do not have access to this label.",
"9001": "The right is invalid.", "9001": "The right is invalid.",
"10001": "The bucket does not exist.", "10001": "The bucket does not exist.",
"10002": "The bucket does not belong to that list.", "10002": "The bucket does not belong to that project.",
"10003": "You cannot remove the last bucket on a list.", "10003": "You cannot remove the last bucket on a project.",
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.", "10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
"10005": "There can be only one done bucket per list.", "10005": "There can be only one done bucket per project.",
"11001": "The saved filter does not exist.", "11001": "The saved filter does not exist.",
"11002": "Saved filters are not available for link shares.", "11002": "Saved filters are not available for link shares.",
"12001": "The subscription entity type is invalid.", "12001": "The subscription entity type is invalid.",

View File

@ -5,7 +5,7 @@ import type {ITask} from './ITask'
export interface IBucket extends IAbstract { export interface IBucket extends IAbstract {
id: number id: number
title: string title: string
listId: number projectId: number
limit: number limit: number
tasks: ITask[] tasks: ITask[]
isDoneBucket: boolean isDoneBucket: boolean

View File

@ -7,7 +7,7 @@ export interface ILabel extends IAbstract {
hexColor: string hexColor: string
description: string description: string
createdBy: IUser createdBy: IUser
listId: number projectId: number
textColor: string textColor: string
created: Date created: Date

View File

@ -8,7 +8,7 @@ export interface ILinkShare extends IAbstract {
right: Right right: Right
sharedBy: IUser sharedBy: IUser
sharingType: number // FIXME: use correct numbers sharingType: number // FIXME: use correct numbers
listId: number projectId: number
name: string name: string
password: string password: string

View File

@ -1,5 +1,5 @@
import type {IAbstract} from './IAbstract' import type {IAbstract} from './IAbstract'
import type {IList} from './IList' import type {IProject} from './IProject'
import type {IUser} from './IUser' import type {IUser} from './IUser'
import type {ISubscription} from './ISubscription' import type {ISubscription} from './ISubscription'
@ -8,7 +8,7 @@ export interface INamespace extends IAbstract {
title: string title: string
description: string description: string
owner: IUser owner: IUser
lists: IList[] projects: IProject[]
isArchived: boolean isArchived: boolean
hexColor: string hexColor: string
subscription: ISubscription subscription: ISubscription

View File

@ -3,13 +3,13 @@ import type {IUser} from './IUser'
import type {ITask} from './ITask' import type {ITask} from './ITask'
import type {ITaskComment} from './ITaskComment' import type {ITaskComment} from './ITaskComment'
import type {ITeam} from './ITeam' import type {ITeam} from './ITeam'
import type { IList } from './IList' import type { IProject } from './IProject'
export const NOTIFICATION_NAMES = { export const NOTIFICATION_NAMES = {
'TASK_COMMENT': 'task.comment', 'TASK_COMMENT': 'task.comment',
'TASK_ASSIGNED': 'task.assigned', 'TASK_ASSIGNED': 'task.assigned',
'TASK_DELETED': 'task.deleted', 'TASK_DELETED': 'task.deleted',
'LIST_CREATED': 'list.created', 'PROJECT_CREATED': 'project.created',
'TEAM_MEMBER_ADDED': 'team.member.added', 'TEAM_MEMBER_ADDED': 'team.member.added',
} as const } as const
@ -32,7 +32,7 @@ interface NotificationDeleted extends Notification {
interface NotificationCreated extends Notification { interface NotificationCreated extends Notification {
task: ITask task: ITask
list: IList project: IProject
} }
interface NotificationMemberAdded extends Notification { interface NotificationMemberAdded extends Notification {

View File

@ -5,7 +5,7 @@ import type {ISubscription} from './ISubscription'
import type {INamespace} from './INamespace' import type {INamespace} from './INamespace'
export interface IList extends IAbstract { export interface IProject extends IAbstract {
id: number id: number
title: string title: string
description: string description: string

View File

@ -1,9 +1,9 @@
import type {IAbstract} from './IAbstract' import type {IAbstract} from './IAbstract'
import type {IList} from './IList' import type {IProject} from './IProject'
import type {INamespace} from './INamespace' import type {INamespace} from './INamespace'
export interface IListDuplicate extends IAbstract { export interface IProjectDuplicate extends IAbstract {
listId: number projectId: number
namespaceId: INamespace['id'] namespaceId: INamespace['id']
list: IList project: IProject
} }

View File

@ -5,7 +5,7 @@ import type {IUser} from './IUser'
import type {ILabel} from './ILabel' import type {ILabel} from './ILabel'
import type {IAttachment} from './IAttachment' import type {IAttachment} from './IAttachment'
import type {ISubscription} from './ISubscription' import type {ISubscription} from './ISubscription'
import type {IList} from './IList' import type {IProject} from './IProject'
import type {IBucket} from './IBucket' import type {IBucket} from './IBucket'
import type {IRelationKind} from '@/types/IRelationKind' import type {IRelationKind} from '@/types/IRelationKind'
@ -49,7 +49,7 @@ export interface ITask extends IAbstract {
created: Date created: Date
updated: Date updated: Date
listId: IList['id'] // Meta, only used when creating a new task projectId: IProject['id'] // Meta, only used when creating a new task
bucketId: IBucket['id'] bucketId: IBucket['id']
} }

View File

@ -1,7 +1,7 @@
import type {IUser} from './IUser' import type {IUser} from './IUser'
import type {IList} from './IList' import type {IProject} from './IProject'
export interface ITeamMember extends IUser { export interface ITeamMember extends IUser {
admin: boolean admin: boolean
teamId: IList['id'] teamId: IProject['id']
} }

View File

@ -1,6 +1,6 @@
import type {ITeamShareBase} from './ITeamShareBase' import type {ITeamShareBase} from './ITeamShareBase'
import type {IList} from './IList' import type {IProject} from './IProject'
export interface ITeamList extends ITeamShareBase { export interface ITeamProject extends ITeamShareBase {
listId: IList['id'] projectId: IProject['id']
} }

View File

@ -1,6 +1,6 @@
import type {IUserShareBase} from './IUserShareBase' import type {IUserShareBase} from './IUserShareBase'
import type {IList} from './IList' import type {IProject} from './IProject'
export interface IUserList extends IUserShareBase { export interface IUserProject extends IUserShareBase {
listId: IList['id'] projectId: IProject['id']
} }

View File

@ -1,6 +1,6 @@
import type {IAbstract} from './IAbstract' import type {IAbstract} from './IAbstract'
import type {IList} from './IList' import type {IProject} from './IProject'
export interface IUserSettings extends IAbstract { export interface IUserSettings extends IAbstract {
name: string name: string
@ -9,7 +9,7 @@ export interface IUserSettings extends IAbstract {
discoverableByEmail: boolean discoverableByEmail: boolean
overdueTasksRemindersEnabled: boolean overdueTasksRemindersEnabled: boolean
overdueTasksRemindersTime: any overdueTasksRemindersTime: any
defaultListId: undefined | IList['id'] defaultProjectId: undefined | IProject['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6 weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string timezone: string
language: string language: string

View File

@ -9,7 +9,7 @@ import type {IUser} from '@/modelTypes/IUser'
export default class BucketModel extends AbstractModel<IBucket> implements IBucket { export default class BucketModel extends AbstractModel<IBucket> implements IBucket {
id = 0 id = 0
title = '' title = ''
listId = '' projectId = ''
limit = 0 limit = 0
tasks: ITask[] = [] tasks: ITask[] = []
isDoneBucket = false isDoneBucket = false

View File

@ -16,7 +16,7 @@ export default class LabelModel extends AbstractModel<ILabel> implements ILabel
hexColor = DEFAULT_LABEL_BACKGROUND_COLOR hexColor = DEFAULT_LABEL_BACKGROUND_COLOR
description = '' description = ''
createdBy: IUser createdBy: IUser
listId = 0 projectId = 0
textColor = '' textColor = ''
created: Date = null created: Date = null

View File

@ -11,7 +11,7 @@ export default class LinkShareModel extends AbstractModel<ILinkShare> implements
right: Right = RIGHTS.READ right: Right = RIGHTS.READ
sharedBy: IUser = UserModel sharedBy: IUser = UserModel
sharingType = 0 // FIXME: use correct numbers sharingType = 0 // FIXME: use correct numbers
listId = 0 projectId = 0
name: '' name: ''
password: '' password: ''
created: Date = null created: Date = null

View File

@ -1,11 +1,11 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import ListModel from './list' import ProjectModel from './project'
import UserModel from './user' import UserModel from './user'
import SubscriptionModel from '@/models/subscription' import SubscriptionModel from '@/models/subscription'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
export default class NamespaceModel extends AbstractModel<INamespace> implements INamespace { export default class NamespaceModel extends AbstractModel<INamespace> implements INamespace {
@ -13,7 +13,7 @@ export default class NamespaceModel extends AbstractModel<INamespace> implements
title = '' title = ''
description = '' description = ''
owner: IUser = UserModel owner: IUser = UserModel
lists: IList[] = [] projects: IProject[] = []
isArchived = false isArchived = false
hexColor = '' hexColor = ''
subscription: ISubscription = null subscription: ISubscription = null
@ -29,8 +29,8 @@ export default class NamespaceModel extends AbstractModel<INamespace> implements
this.hexColor = '#' + this.hexColor this.hexColor = '#' + this.hexColor
} }
this.lists = this.lists.map(l => { this.projects = this.projects.map(l => {
return new ListModel(l) return new ProjectModel(l)
}) })
this.owner = new UserModel(this.owner) this.owner = new UserModel(this.owner)

View File

@ -3,7 +3,7 @@ import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import UserModel, {getDisplayName} from '@/models/user' import UserModel, {getDisplayName} from '@/models/user'
import TaskModel from '@/models/task' import TaskModel from '@/models/task'
import TaskCommentModel from '@/models/taskComment' import TaskCommentModel from '@/models/taskComment'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
import {NOTIFICATION_NAMES, type INotification} from '@/modelTypes/INotification' import {NOTIFICATION_NAMES, type INotification} from '@/modelTypes/INotification'
@ -43,10 +43,10 @@ export default class NotificationModel extends AbstractModel<INotification> impl
task: new TaskModel(this.notification.task), task: new TaskModel(this.notification.task),
} }
break break
case NOTIFICATION_NAMES.LIST_CREATED: case NOTIFICATION_NAMES.PROJECT_CREATED:
this.notification = { this.notification = {
doer: new UserModel(this.notification.doer), doer: new UserModel(this.notification.doer),
list: new ListModel(this.notification.list), project: new ProjectModel(this.notification.project),
} }
break break
case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED: case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED:
@ -78,8 +78,8 @@ export default class NotificationModel extends AbstractModel<INotification> impl
return `assigned ${who} to ${this.notification.task.getTextIdentifier()}` return `assigned ${who} to ${this.notification.task.getTextIdentifier()}`
case NOTIFICATION_NAMES.TASK_DELETED: case NOTIFICATION_NAMES.TASK_DELETED:
return `deleted ${this.notification.task.getTextIdentifier()}` return `deleted ${this.notification.task.getTextIdentifier()}`
case NOTIFICATION_NAMES.LIST_CREATED: case NOTIFICATION_NAMES.PROJECT_CREATED:
return `created ${this.notification.list.title}` return `created ${this.notification.project.title}`
case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED: case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED:
who = `${getDisplayName(this.notification.member)}` who = `${getDisplayName(this.notification.member)}`

View File

@ -3,13 +3,13 @@ import TaskModel from '@/models/task'
import UserModel from '@/models/user' import UserModel from '@/models/user'
import SubscriptionModel from '@/models/subscription' import SubscriptionModel from '@/models/subscription'
import type {IList} from '@/modelTypes/IList' 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 {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
export default class ListModel extends AbstractModel<IList> implements IList { export default class ProjectModel extends AbstractModel<IProject> implements IProject {
id = 0 id = 0
title = '' title = ''
description = '' description = ''
@ -28,7 +28,7 @@ export default class ListModel extends AbstractModel<IList> implements IList {
created: Date = null created: Date = null
updated: Date = null updated: Date = null
constructor(data: Partial<IList> = {}) { constructor(data: Partial<IProject> = {}) {
super() super()
this.assignData(data) this.assignData(data)

View File

@ -1,19 +1,19 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import ListModel from './list' import ProjectModel from './project'
import type {IListDuplicate} from '@/modelTypes/IListDuplicate' import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
export default class ListDuplicateModel extends AbstractModel<IListDuplicate> implements IListDuplicate { export default class ProjectDuplicateModel extends AbstractModel<IProjectDuplicate> implements IProjectDuplicate {
listId = 0 projectId = 0
namespaceId: INamespace['id'] = 0 namespaceId: INamespace['id'] = 0
list: IList = ListModel project: IProject = ProjectModel
constructor(data : Partial<IListDuplicate>) { constructor(data : Partial<IProjectDuplicate>) {
super() super()
this.assignData(data) this.assignData(data)
this.list = new ListModel(this.list) this.project = new ProjectModel(this.project)
} }
} }

View File

@ -5,7 +5,7 @@ import type {ITask} from '@/modelTypes/ITask'
import type {ILabel} from '@/modelTypes/ILabel' import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
import type {IBucket} from '@/modelTypes/IBucket' import type {IBucket} from '@/modelTypes/IBucket'
@ -93,7 +93,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
created: Date = null created: Date = null
updated: Date = null updated: Date = null
listId: IList['id'] = 0 projectId: IProject['id'] = 0
bucketId: IBucket['id'] = 0 bucketId: IBucket['id'] = 0
constructor(data: Partial<ITask> = {}) { constructor(data: Partial<ITask> = {}) {
@ -142,7 +142,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
// Make all attachments to attachment models // Make all attachments to attachment models
this.attachments = this.attachments.map(a => new AttachmentModel(a)) this.attachments = this.attachments.map(a => new AttachmentModel(a))
// Set the task identifier to empty if the list does not have one // Set the task identifier to empty if the project does not have one
if (this.identifier === `-${this.index}`) { if (this.identifier === `-${this.index}`) {
this.identifier = '' this.identifier = ''
} }
@ -155,7 +155,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
this.created = new Date(this.created) this.created = new Date(this.created)
this.updated = new Date(this.updated) this.updated = new Date(this.updated)
this.listId = Number(this.listId) this.projectId = Number(this.projectId)
} }
getTextIdentifier() { getTextIdentifier() {

View File

@ -1,11 +1,11 @@
import UserModel from './user' import UserModel from './user'
import type {ITeamMember} from '@/modelTypes/ITeamMember' import type {ITeamMember} from '@/modelTypes/ITeamMember'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
export default class TeamMemberModel extends UserModel implements ITeamMember { export default class TeamMemberModel extends UserModel implements ITeamMember {
admin = false admin = false
teamId: IList['id'] = 0 teamId: IProject['id'] = 0
constructor(data: Partial<ITeamMember>) { constructor(data: Partial<ITeamMember>) {
super(data) super(data)

View File

@ -1,12 +1,12 @@
import TeamShareBaseModel from './teamShareBase' import TeamShareBaseModel from './teamShareBase'
import type {ITeamList} from '@/modelTypes/ITeamList' import type {ITeamProject} from '@/modelTypes/ITeamProject'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
export default class TeamListModel extends TeamShareBaseModel implements ITeamList { export default class TeamProjectModel extends TeamShareBaseModel implements ITeamProject {
listId: IList['id'] = 0 projectId: IProject['id'] = 0
constructor(data: Partial<ITeamList>) { constructor(data: Partial<ITeamProject>) {
super(data) super(data)
this.assignData(data) this.assignData(data)
} }

View File

@ -6,7 +6,7 @@ import type {ITeam} from '@/modelTypes/ITeam'
/** /**
* This class is a base class for common team sharing model. * This class is a base class for common team sharing model.
* It is extended in a way so it can be used for namespaces as well for lists. * It is extended in a way so it can be used for namespaces as well for projects.
*/ */
export default class TeamShareBaseModel extends AbstractModel<ITeamShareBase> implements ITeamShareBase { export default class TeamShareBaseModel extends AbstractModel<ITeamShareBase> implements ITeamShareBase {
teamId: ITeam['id'] = 0 teamId: ITeam['id'] = 0

View File

@ -1,13 +1,13 @@
import UserShareBaseModel from './userShareBase' import UserShareBaseModel from './userShareBase'
import type {IUserList} from '@/modelTypes/IUserList' import type {IUserProject} from '@/modelTypes/IUserProject'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
// This class extends the user share model with a 'rights' parameter which is used in sharing // This class extends the user share model with a 'rights' parameter which is used in sharing
export default class UserListModel extends UserShareBaseModel implements IUserList { export default class UserProjectModel extends UserShareBaseModel implements IUserProject {
listId: IList['id'] = 0 projectId: IProject['id'] = 0
constructor(data: Partial<IUserList>) { constructor(data: Partial<IUserProject>) {
super(data) super(data)
this.assignData(data) this.assignData(data)
} }

View File

@ -10,7 +10,7 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
discoverableByEmail = false discoverableByEmail = false
overdueTasksRemindersEnabled = true overdueTasksRemindersEnabled = true
overdueTasksRemindersTime = undefined overdueTasksRemindersTime = undefined
defaultListId = undefined defaultProjectId = undefined
weekStart = 0 as IUserSettings['weekStart'] weekStart = 0 as IUserSettings['weekStart']
timezone = '' timezone = ''
language = getCurrentLanguage() language = getCurrentLanguage()

View File

@ -21,14 +21,14 @@ describe('Parse Task Text', () => {
}) })
it('should not parse text when disabled', () => { it('should not parse text when disabled', () => {
const text = 'Lorem Ipsum today *label +list !2 @user' const text = 'Lorem Ipsum today *label +project !2 @user'
const result = parseTaskText(text, PrefixMode.Disabled) const result = parseTaskText(text, PrefixMode.Disabled)
expect(result.text).toBe(text) expect(result.text).toBe(text)
}) })
it('should parse text in todoist mode when configured', () => { it('should parse text in todoist mode when configured', () => {
const result = parseTaskText('Lorem Ipsum today @label #list !2 +user', PrefixMode.Todoist) const result = parseTaskText('Lorem Ipsum today @label #project !2 +user', PrefixMode.Todoist)
expect(result.text).toBe('Lorem Ipsum +user') expect(result.text).toBe('Lorem Ipsum +user')
const now = new Date() const now = new Date()
@ -37,7 +37,7 @@ describe('Parse Task Text', () => {
expect(result?.date?.getDate()).toBe(now.getDate()) expect(result?.date?.getDate()).toBe(now.getDate())
expect(result.labels).toHaveLength(1) expect(result.labels).toHaveLength(1)
expect(result.labels[0]).toBe('label') expect(result.labels[0]).toBe('label')
expect(result.list).toBe('list') expect(result.project).toBe('project')
expect(result.priority).toBe(2) expect(result.priority).toBe(2)
expect(result.assignees).toHaveLength(1) expect(result.assignees).toHaveLength(1)
expect(result.assignees[0]).toBe('user') expect(result.assignees[0]).toBe('user')
@ -574,36 +574,36 @@ describe('Parse Task Text', () => {
}) })
}) })
describe('List', () => { describe('Project', () => {
it('should parse a list', () => { it('should parse a project', () => {
const result = parseTaskText('Lorem Ipsum +list') const result = parseTaskText('Lorem Ipsum +project')
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.list).toBe('list') expect(result.project).toBe('project')
}) })
it('should parse a list with a space in it', () => { it('should parse a project with a space in it', () => {
const result = parseTaskText(`Lorem Ipsum +'list with long name'`) const result = parseTaskText(`Lorem Ipsum +'project with long name'`)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.list).toBe('list with long name') expect(result.project).toBe('project with long name')
}) })
it('should parse a list with a space in it and "', () => { it('should parse a project with a space in it and "', () => {
const result = parseTaskText(`Lorem Ipsum +"list with long name"`) const result = parseTaskText(`Lorem Ipsum +"project with long name"`)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.list).toBe('list with long name') expect(result.project).toBe('project with long name')
}) })
it('should parse only the first list', () => { it('should parse only the first project', () => {
const result = parseTaskText(`Lorem Ipsum +list1 +list2 +list3`) const result = parseTaskText(`Lorem Ipsum +project1 +project2 +project3`)
expect(result.text).toBe('Lorem Ipsum +list2 +list3') expect(result.text).toBe('Lorem Ipsum +project2 +project3')
expect(result.list).toBe('list1') expect(result.project).toBe('project1')
}) })
it('should parse a list that\'s called like a date as list', () => { it('should parse a project that\'s called like a date as project', () => {
const result = parseTaskText(`Lorem Ipsum +today`) const result = parseTaskText(`Lorem Ipsum +today`)
expect(result.text).toBe('Lorem Ipsum') expect(result.text).toBe('Lorem Ipsum')
expect(result.list).toBe('today') expect(result.project).toBe('today')
}) })
}) })

View File

@ -5,14 +5,14 @@ import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
const VIKUNJA_PREFIXES: Prefixes = { const VIKUNJA_PREFIXES: Prefixes = {
label: '*', label: '*',
list: '+', project: '+',
priority: '!', priority: '!',
assignee: '@', assignee: '@',
} }
const TODOIST_PREFIXES: Prefixes = { const TODOIST_PREFIXES: Prefixes = {
label: '@', label: '@',
list: '#', project: '#',
priority: '!', priority: '!',
assignee: '+', assignee: '+',
} }
@ -38,7 +38,7 @@ export interface ParsedTaskText {
text: string, text: string,
date: Date | null, date: Date | null,
labels: string[], labels: string[],
list: string | null, project: string | null,
priority: number | null, priority: number | null,
assignees: string[], assignees: string[],
repeats: IRepeatAfter | null, repeats: IRepeatAfter | null,
@ -46,13 +46,13 @@ export interface ParsedTaskText {
interface Prefixes { interface Prefixes {
label: string, label: string,
list: string, project: string,
priority: string, priority: string,
assignee: string, assignee: string,
} }
/** /**
* Parses task text for dates, assignees, labels, lists, priorities and returns an object with all found intents. * Parses task text for dates, assignees, labels, projects, priorities and returns an object with all found intents.
* *
* @param text * @param text
*/ */
@ -61,7 +61,7 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
text: text, text: text,
date: null, date: null,
labels: [], labels: [],
list: null, project: null,
priority: null, priority: null,
assignees: [], assignees: [],
repeats: null, repeats: null,
@ -75,8 +75,8 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
result.labels = getLabelsFromPrefix(text, prefixes.label) ?? [] result.labels = getLabelsFromPrefix(text, prefixes.label) ?? []
result.text = cleanupItemText(result.text, result.labels, prefixes.label) result.text = cleanupItemText(result.text, result.labels, prefixes.label)
result.list = getListFromPrefix(result.text, prefixes.list) result.project = getProjectFromPrefix(result.text, prefixes.project)
result.text = result.list !== null ? cleanupItemText(result.text, [result.list], prefixes.list) : result.text result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
result.priority = getPriority(result.text, prefixes.priority) result.priority = getPriority(result.text, prefixes.priority)
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text
@ -129,27 +129,27 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
return Array.from(new Set(items)) return Array.from(new Set(items))
} }
export const getListFromPrefix = (text: string, listPrefix: string | null = null): string | null => { export const getProjectFromPrefix = (text: string, projectPrefix: string | null = null): string | null => {
if (listPrefix === null) { if (projectPrefix === null) {
const prefixes = PREFIXES[getQuickAddMagicMode()] const prefixes = PREFIXES[getQuickAddMagicMode()]
if (prefixes === undefined) { if (prefixes === undefined) {
return null return null
} }
listPrefix = prefixes.list projectPrefix = prefixes.project
} }
const lists: string[] = getItemsFromPrefix(text, listPrefix) const projects: string[] = getItemsFromPrefix(text, projectPrefix)
return lists.length > 0 ? lists[0] : null return projects.length > 0 ? projects[0] : null
} }
export const getLabelsFromPrefix = (text: string, listPrefix: string | null = null): string[] | null => { export const getLabelsFromPrefix = (text: string, projectPrefix: string | null = null): string[] | null => {
if (listPrefix === null) { if (projectPrefix === null) {
const prefixes = PREFIXES[getQuickAddMagicMode()] const prefixes = PREFIXES[getQuickAddMagicMode()]
if (prefixes === undefined) { if (prefixes === undefined) {
return null return null
} }
listPrefix = prefixes.label projectPrefix = prefixes.label
} }
return getItemsFromPrefix(text, listPrefix) return getItemsFromPrefix(text, projectPrefix)
} }
const getPriority = (text: string, prefix: string): number | null => { const getPriority = (text: string, prefix: string): number | null => {
@ -291,7 +291,7 @@ export const cleanupItemText = (text: string, items: string[], prefix: string):
const cleanupResult = (result: ParsedTaskText, prefixes: Prefixes): ParsedTaskText => { const cleanupResult = (result: ParsedTaskText, prefixes: Prefixes): ParsedTaskText => {
result.text = cleanupItemText(result.text, result.labels, prefixes.label) result.text = cleanupItemText(result.text, result.labels, prefixes.label)
result.text = result.list !== null ? cleanupItemText(result.text, [result.list], prefixes.list) : result.text result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text
// Not removing assignees to avoid removing @text where the user does not exist // Not removing assignees to avoid removing @text where the user does not exist
result.text = result.text.trim() result.text = result.text.trim()

View File

@ -1,5 +1,5 @@
import {test, expect, vi} from 'vitest' import {test, expect, vi} from 'vitest'
import {getHistory, removeListFromHistory, saveListToHistory} from './listHistory' import {getHistory, removeProjectFromHistory, saveProjectToHistory} from './projectHistory'
test('return an empty history when none was saved', () => { test('return an empty history when none was saved', () => {
Storage.prototype.getItem = vi.fn(() => null) Storage.prototype.getItem = vi.fn(() => null)
@ -15,68 +15,68 @@ test('return a saved history', () => {
expect(h).toStrictEqual(saved) expect(h).toStrictEqual(saved)
}) })
test('store list in history', () => { test('store project in history', () => {
let saved = {} let saved = {}
Storage.prototype.getItem = vi.fn(() => null) Storage.prototype.getItem = vi.fn(() => null)
Storage.prototype.setItem = vi.fn((key, lists) => { Storage.prototype.setItem = vi.fn((key, projects) => {
saved = lists saved = projects
}) })
saveListToHistory({id: 1}) saveProjectToHistory({id: 1})
expect(saved).toBe('[{"id":1}]') expect(saved).toBe('[{"id":1}]')
}) })
test('store only the last 5 lists in history', () => { test('store only the last 5 projects in history', () => {
let saved: string | null = null let saved: string | null = null
Storage.prototype.getItem = vi.fn(() => saved) Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, lists: string) => { Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
saved = lists saved = projects
}) })
saveListToHistory({id: 1}) saveProjectToHistory({id: 1})
saveListToHistory({id: 2}) saveProjectToHistory({id: 2})
saveListToHistory({id: 3}) saveProjectToHistory({id: 3})
saveListToHistory({id: 4}) saveProjectToHistory({id: 4})
saveListToHistory({id: 5}) saveProjectToHistory({id: 5})
saveListToHistory({id: 6}) saveProjectToHistory({id: 6})
expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]') expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
}) })
test('don\'t store the same list twice', () => { test('don\'t store the same project twice', () => {
let saved: string | null = null let saved: string | null = null
Storage.prototype.getItem = vi.fn(() => saved) Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, lists: string) => { Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
saved = lists saved = projects
}) })
saveListToHistory({id: 1}) saveProjectToHistory({id: 1})
saveListToHistory({id: 1}) saveProjectToHistory({id: 1})
expect(saved).toBe('[{"id":1}]') expect(saved).toBe('[{"id":1}]')
}) })
test('move a list to the beginning when storing it multiple times', () => { test('move a project to the beginning when storing it multiple times', () => {
let saved: string | null = null let saved: string | null = null
Storage.prototype.getItem = vi.fn(() => saved) Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, lists: string) => { Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
saved = lists saved = projects
}) })
saveListToHistory({id: 1}) saveProjectToHistory({id: 1})
saveListToHistory({id: 2}) saveProjectToHistory({id: 2})
saveListToHistory({id: 1}) saveProjectToHistory({id: 1})
expect(saved).toBe('[{"id":1},{"id":2}]') expect(saved).toBe('[{"id":1},{"id":2}]')
}) })
test('remove list from history', () => { test('remove project from history', () => {
let saved: string | null = '[{"id": 1}]' let saved: string | null = '[{"id": 1}]'
Storage.prototype.getItem = vi.fn(() => null) Storage.prototype.getItem = vi.fn(() => null)
Storage.prototype.setItem = vi.fn((key: string, lists: string) => { Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
saved = lists saved = projects
}) })
Storage.prototype.removeItem = vi.fn((key: string) => { Storage.prototype.removeItem = vi.fn((key: string) => {
saved = null saved = null
}) })
removeListFromHistory({id: 1}) removeProjectFromHistory({id: 1})
expect(saved).toBeNull() expect(saved).toBeNull()
}) })

View File

@ -1,9 +1,9 @@
export interface ListHistory { export interface ProjectHistory {
id: number; id: number;
} }
export function getHistory(): ListHistory[] { export function getHistory(): ProjectHistory[] {
const savedHistory = localStorage.getItem('listHistory') const savedHistory = localStorage.getItem('projectHistory')
if (savedHistory === null) { if (savedHistory === null) {
return [] return []
} }
@ -11,27 +11,27 @@ export function getHistory(): ListHistory[] {
return JSON.parse(savedHistory) return JSON.parse(savedHistory)
} }
function saveHistory(history: ListHistory[]) { function saveHistory(history: ProjectHistory[]) {
if (history.length === 0) { if (history.length === 0) {
localStorage.removeItem('listHistory') localStorage.removeItem('projectHistory')
return return
} }
localStorage.setItem('listHistory', JSON.stringify(history)) localStorage.setItem('projectHistory', JSON.stringify(history))
} }
export function saveListToHistory(list: ListHistory) { export function saveProjectToHistory(project: ProjectHistory) {
const history: ListHistory[] = getHistory() const history: ProjectHistory[] = getHistory()
// Remove the element if it already exists in history, preventing duplicates and essentially moving it to the beginning // Remove the element if it already exists in history, preventing duplicates and essentially moving it to the beginning
history.forEach((l, i) => { history.forEach((l, i) => {
if (l.id === list.id) { if (l.id === project.id) {
history.splice(i, 1) history.splice(i, 1)
} }
}) })
// Add the new list to the beginning of the list // Add the new project to the beginning of the project
history.unshift(list) history.unshift(project)
if (history.length > 5) { if (history.length > 5) {
history.pop() history.pop()
@ -39,11 +39,11 @@ export function saveListToHistory(list: ListHistory) {
saveHistory(history) saveHistory(history)
} }
export function removeListFromHistory(list: ListHistory) { export function removeProjectFromHistory(project: ProjectHistory) {
const history: ListHistory[] = getHistory() const history: ProjectHistory[] = getHistory()
history.forEach((l, i) => { history.forEach((l, i) => {
if (l.id === list.id) { if (l.id === project.id) {
history.splice(i, 1) history.splice(i, 1)
} }
}) })

View File

@ -2,12 +2,12 @@ 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 {saveListView, getListView} from '@/helpers/saveListView' import {saveProjectView, getProjectView} from '@/helpers/saveProjectView'
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 {setTitle} from '@/helpers/setTitle'
import {useListStore} from '@/stores/lists' 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,20 +33,20 @@ 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')
// List Views // Project Views
const ListList = () => import('@/views/list/ListList.vue') const ProjectList = () => import('@/views/project/ProjectList.vue')
const ListGantt = () => import('@/views/list/ListGantt.vue') const ProjectGantt = () => import('@/views/project/ProjectGantt.vue')
const ListTable = () => import('@/views/list/ListTable.vue') const ProjectTable = () => import('@/views/project/ProjectTable.vue')
const ListKanban = () => import('@/views/list/ListKanban.vue') const ProjectKanban = () => import('@/views/project/ProjectKanban.vue')
const ListInfo = () => import('@/views/list/ListInfo.vue') const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
// List Settings // Project Settings
const ListSettingEdit = () => import('@/views/list/settings/edit.vue') const ProjectSettingEdit = () => import('@/views/project/settings/edit.vue')
const ListSettingBackground = () => import('@/views/list/settings/background.vue') const ProjectSettingBackground = () => import('@/views/project/settings/background.vue')
const ListSettingDuplicate = () => import('@/views/list/settings/duplicate.vue') const ProjectSettingDuplicate = () => import('@/views/project/settings/duplicate.vue')
const ListSettingShare = () => import('@/views/list/settings/share.vue') const ProjectSettingShare = () => import('@/views/project/settings/share.vue')
const ListSettingDelete = () => import('@/views/list/settings/delete.vue') const ProjectSettingDelete = () => import('@/views/project/settings/delete.vue')
const ListSettingArchive = () => import('@/views/list/settings/archive.vue') const ProjectSettingArchive = () => import('@/views/project/settings/archive.vue')
// Namespace Settings // Namespace Settings
const NamespaceSettingEdit = () => import('@/views/namespaces/settings/edit.vue') const NamespaceSettingEdit = () => import('@/views/namespaces/settings/edit.vue')
@ -71,8 +71,8 @@ const UserSettingsGeneralComponent = () => import('@/views/user/settings/General
const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue') const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue')
const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue') const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
// List Handling // Project Handling
const NewListComponent = () => import('@/views/list/NewList.vue') const NewProjectComponent = () => import('@/views/project/NewProject.vue')
// Namespace Handling // Namespace Handling
const NewNamespaceComponent = () => import('@/views/namespaces/NewNamespace.vue') const NewNamespaceComponent = () => import('@/views/namespaces/NewNamespace.vue')
@ -269,142 +269,142 @@ const router = createRouter({
}), }),
}, },
{ {
path: '/lists/new/:namespaceId/', path: '/projects/new/:namespaceId/',
name: 'list.create', name: 'project.create',
component: NewListComponent, component: NewProjectComponent,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
}, },
{ {
path: '/lists/:listId/settings/edit', path: '/projects/:projectId/settings/edit',
name: 'list.settings.edit', name: 'project.settings.edit',
component: ListSettingEdit, component: ProjectSettingEdit,
props: route => ({ listId: Number(route.params.listId as string) }), props: route => ({ projectId: Number(route.params.projectId as string) }),
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
}, },
{ {
path: '/lists/:listId/settings/background', path: '/projects/:projectId/settings/background',
name: 'list.settings.background', name: 'project.settings.background',
component: ListSettingBackground, component: ProjectSettingBackground,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
}, },
{ {
path: '/lists/:listId/settings/duplicate', path: '/projects/:projectId/settings/duplicate',
name: 'list.settings.duplicate', name: 'project.settings.duplicate',
component: ListSettingDuplicate, component: ProjectSettingDuplicate,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
}, },
{ {
path: '/lists/:listId/settings/share', path: '/projects/:projectId/settings/share',
name: 'list.settings.share', name: 'project.settings.share',
component: ListSettingShare, component: ProjectSettingShare,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
}, },
{ {
path: '/lists/:listId/settings/delete', path: '/projects/:projectId/settings/delete',
name: 'list.settings.delete', name: 'project.settings.delete',
component: ListSettingDelete, component: ProjectSettingDelete,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
}, },
{ {
path: '/lists/:listId/settings/archive', path: '/projects/:projectId/settings/archive',
name: 'list.settings.archive', name: 'project.settings.archive',
component: ListSettingArchive, component: ProjectSettingArchive,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
}, },
{ {
path: '/lists/:listId/settings/edit', path: '/projects/:projectId/settings/edit',
name: 'filter.settings.edit', name: 'filter.settings.edit',
component: FilterEdit, component: FilterEdit,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
props: route => ({ listId: Number(route.params.listId as string) }), props: route => ({ projectId: Number(route.params.projectId as string) }),
}, },
{ {
path: '/lists/:listId/settings/delete', path: '/projects/:projectId/settings/delete',
name: 'filter.settings.delete', name: 'filter.settings.delete',
component: FilterDelete, component: FilterDelete,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
props: route => ({ listId: Number(route.params.listId as string) }), props: route => ({ projectId: Number(route.params.projectId as string) }),
}, },
{ {
path: '/lists/:listId/info', path: '/projects/:projectId/info',
name: 'list.info', name: 'project.info',
component: ListInfo, component: ProjectInfo,
meta: { meta: {
showAsModal: true, showAsModal: true,
}, },
props: route => ({ listId: Number(route.params.listId as string) }), props: route => ({ projectId: Number(route.params.projectId as string) }),
}, },
{ {
path: '/lists/:listId', path: '/projects/:projectId',
name: 'list.index', name: 'project.index',
redirect(to) { redirect(to) {
// Redirect the user to list view by default // Redirect the user to list view by default
const savedListView = getListView(to.params.listId) const savedProjectView = getProjectView(to.params.projectId)
console.debug('Replaced list view with', savedListView) console.debug('Replaced list view with', savedProjectView)
return { return {
name: router.hasRoute(savedListView) name: router.hasRoute(savedProjectView)
? savedListView ? savedProjectView
: 'list.list', : 'project.list',
params: {listId: to.params.listId}, params: {projectId: to.params.projectId},
} }
}, },
}, },
{ {
path: '/lists/:listId/list', path: '/projects/:projectId/list',
name: 'list.list', name: 'project.list',
component: ListList, component: ProjectList,
beforeEnter: (to) => saveListView(to.params.listId, to.name), beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
props: route => ({ listId: Number(route.params.listId as string) }), props: route => ({ projectId: Number(route.params.projectId as string) }),
}, },
{ {
path: '/lists/:listId/gantt', path: '/projects/:projectId/gantt',
name: 'list.gantt', name: 'project.gantt',
component: ListGantt, component: ProjectGantt,
beforeEnter: (to) => saveListView(to.params.listId, to.name), beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
// FIXME: test if `useRoute` would be the same. If it would use it instead. // FIXME: test if `useRoute` would be the same. If it would use it instead.
props: route => ({route}), props: route => ({route}),
}, },
{ {
path: '/lists/:listId/table', path: '/projects/:projectId/table',
name: 'list.table', name: 'project.table',
component: ListTable, component: ProjectTable,
beforeEnter: (to) => saveListView(to.params.listId, to.name), beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
props: route => ({ listId: Number(route.params.listId as string) }), props: route => ({ projectId: Number(route.params.projectId as string) }),
}, },
{ {
path: '/lists/:listId/kanban', path: '/projects/:projectId/kanban',
name: 'list.kanban', name: 'project.kanban',
component: ListKanban, component: ProjectKanban,
beforeEnter: (to) => { beforeEnter: (to) => {
saveListView(to.params.listId, to.name) saveProjectView(to.params.projectId, to.name)
// Properly set the page title when a task popup is closed // Properly set the page title when a task popup is closed
const listStore = useListStore() const projectStore = useProjectStore()
const listFromStore = listStore.getListById(Number(to.params.listId)) const projectFromStore = projectStore.getProjectById(Number(to.params.projectId))
if(listFromStore) { if(projectFromStore) {
setTitle(listFromStore.title) setTitle(projectFromStore.title)
} }
}, },
props: route => ({ listId: Number(route.params.listId as string) }), props: route => ({ projectId: Number(route.params.projectId as string) }),
}, },
{ {
path: '/teams', path: '/teams',

View File

@ -1,13 +1,13 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import BackgroundImageModel from '../models/backgroundImage' import BackgroundImageModel from '../models/backgroundImage'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import type { IBackgroundImage } from '@/modelTypes/IBackgroundImage' import type { IBackgroundImage } from '@/modelTypes/IBackgroundImage'
export default class BackgroundUnsplashService extends AbstractService<IBackgroundImage> { export default class BackgroundUnsplashService extends AbstractService<IBackgroundImage> {
constructor() { constructor() {
super({ super({
getAll: '/backgrounds/unsplash/search', getAll: '/backgrounds/unsplash/search',
update: '/lists/{listId}/backgrounds/unsplash', update: '/projects/{projectId}/backgrounds/unsplash',
}) })
} }
@ -16,7 +16,7 @@ export default class BackgroundUnsplashService extends AbstractService<IBackgrou
} }
modelUpdateFactory(data) { modelUpdateFactory(data) {
return new ListModel(data) return new ProjectModel(data)
} }
async thumb(model) { async thumb(model) {

View File

@ -1,13 +1,13 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import type { IList } from '@/modelTypes/IList' import type { IProject } from '@/modelTypes/IProject'
import type { IFile } from '@/modelTypes/IFile' import type { IFile } from '@/modelTypes/IFile'
export default class BackgroundUploadService extends AbstractService { export default class BackgroundUploadService extends AbstractService {
constructor() { constructor() {
super({ super({
create: '/lists/{listId}/backgrounds/upload', create: '/projects/{projectId}/backgrounds/upload',
}) })
} }
@ -15,16 +15,16 @@ export default class BackgroundUploadService extends AbstractService {
return false return false
} }
modelCreateFactory(data: Partial<IList>) { modelCreateFactory(data: Partial<IProject>) {
return new ListModel(data) return new ProjectModel(data)
} }
/** /**
* Uploads a file to the server * Uploads a file to the server
*/ */
create(listId: IList['id'], file: IFile) { create(projectId: IProject['id'], file: IFile) {
return this.uploadFile( return this.uploadFile(
this.getReplacedRoute(this.paths.create, {listId}), this.getReplacedRoute(this.paths.create, {projectId}),
file, file,
'background', 'background',
) )

View File

@ -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: '/lists/{listId}/buckets', getAll: '/projects/{projectId}/buckets',
create: '/lists/{listId}/buckets', create: '/projects/{projectId}/buckets',
update: '/lists/{listId}/buckets/{id}', update: '/projects/{projectId}/buckets/{id}',
delete: '/lists/{listId}/buckets/{id}', delete: '/projects/{projectId}/buckets/{id}',
}) })
} }

View File

@ -5,10 +5,10 @@ import type {ILinkShare} from '@/modelTypes/ILinkShare'
export default class LinkShareService extends AbstractService<ILinkShare> { export default class LinkShareService extends AbstractService<ILinkShare> {
constructor() { constructor() {
super({ super({
getAll: '/lists/{listId}/shares', getAll: '/projects/{projectId}/shares',
get: '/lists/{listId}/shares/{id}', get: '/projects/{projectId}/shares/{id}',
create: '/lists/{listId}/shares', create: '/projects/{projectId}/shares',
delete: '/lists/{listId}/shares/{id}', delete: '/projects/{projectId}/shares/{id}',
}) })
} }

View File

@ -1,22 +1,22 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import TaskService from './task' import TaskService from './task'
import {colorFromHex} from '@/helpers/color/colorFromHex' import {colorFromHex} from '@/helpers/color/colorFromHex'
export default class ListService extends AbstractService<IList> { export default class ProjectService extends AbstractService<IProject> {
constructor() { constructor() {
super({ super({
create: '/namespaces/{namespaceId}/lists', create: '/namespaces/{namespaceId}/projects',
get: '/lists/{id}', get: '/projects/{id}',
getAll: '/lists', getAll: '/projects',
update: '/lists/{id}', update: '/projects/{id}',
delete: '/lists/{id}', delete: '/projects/{id}',
}) })
} }
modelFactory(data) { modelFactory(data) {
return new ListModel(data) return new ProjectModel(data)
} }
beforeUpdate(model) { beforeUpdate(model) {
@ -34,29 +34,29 @@ export default class ListService extends AbstractService<IList> {
return model return model
} }
beforeCreate(list) { beforeCreate(project) {
list.hexColor = colorFromHex(list.hexColor) project.hexColor = colorFromHex(project.hexColor)
return list return project
} }
async background(list: Pick<IList, 'id' | 'backgroundInformation'>) { async background(project: Pick<IProject, 'id' | 'backgroundInformation'>) {
if (list.backgroundInformation === null) { if (project.backgroundInformation === null) {
return '' return ''
} }
const response = await this.http({ const response = await this.http({
url: `/lists/${list.id}/background`, url: `/projects/${project.id}/background`,
method: 'GET', method: 'GET',
responseType: 'blob', responseType: 'blob',
}) })
return window.URL.createObjectURL(new Blob([response.data])) return window.URL.createObjectURL(new Blob([response.data]))
} }
async removeBackground(list: Pick<IList, 'id'>) { async removeBackground(project: Pick<IProject, 'id'>) {
const cancel = this.setLoading() const cancel = this.setLoading()
try { try {
const response = await this.http.delete(`/lists/${list.id}/background`, list) const response = await this.http.delete(`/projects/${project.id}/background`, project)
return response.data return response.data
} finally { } finally {
cancel() cancel()

View File

@ -1,21 +1,21 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import listDuplicateModel from '@/models/listDuplicateModel' import projectDuplicateModel from '@/models/projectDuplicateModel'
import type {IListDuplicate} from '@/modelTypes/IListDuplicate' import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate'
export default class ListDuplicateService extends AbstractService<IListDuplicate> { export default class ProjectDuplicateService extends AbstractService<IProjectDuplicate> {
constructor() { constructor() {
super({ super({
create: '/lists/{listId}/duplicate', create: '/projects/{projectId}/duplicate',
}) })
} }
beforeCreate(model) { beforeCreate(model) {
model.list = null model.project = null
return model return model
} }
modelFactory(data) { modelFactory(data) {
return new listDuplicateModel(data) return new projectDuplicateModel(data)
} }
} }

View File

@ -1,10 +1,10 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import UserModel from '../models/user' import UserModel from '../models/user'
export default class ListUserService extends AbstractService { export default class ProjectUserService extends AbstractService {
constructor() { constructor() {
super({ super({
getAll: '/lists/{listId}/listusers', getAll: '/projects/{projectId}/projectusers',
}) })
} }

View File

@ -4,7 +4,7 @@ import {useI18n} from 'vue-i18n'
import type {MaybeRef} from '@vueuse/core' import type {MaybeRef} from '@vueuse/core'
import {useDebounceFn} from '@vueuse/core' import {useDebounceFn} from '@vueuse/core'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import type {ISavedFilter} from '@/modelTypes/ISavedFilter' import type {ISavedFilter} from '@/modelTypes/ISavedFilter'
import AbstractService from '@/services/abstractService' import AbstractService from '@/services/abstractService'
@ -16,31 +16,31 @@ import {useNamespaceStore} from '@/stores/namespaces'
import {objectToSnakeCase, objectToCamelCase} from '@/helpers/case' import {objectToSnakeCase, objectToCamelCase} from '@/helpers/case'
import {success} from '@/message' import {success} from '@/message'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
/** /**
* Calculates the corresponding list id to this saved filter. * Calculates the corresponding project id to this saved filter.
* This function matches the one in the api. * This function matches the one in the api.
*/ */
function getListId(savedFilter: ISavedFilter) { function getProjectId(savedFilter: ISavedFilter) {
let listId = savedFilter.id * -1 - 1 let projectId = savedFilter.id * -1 - 1
if (listId > 0) { if (projectId > 0) {
listId = 0 projectId = 0
} }
return listId return projectId
} }
export function getSavedFilterIdFromListId(listId: IList['id']) { export function getSavedFilterIdFromProjectId(projectId: IProject['id']) {
let filterId = listId * -1 - 1 let filterId = projectId * -1 - 1
// FilterIds from listIds are always positive // FilterIds from projectIds are always positive
if (filterId < 0) { if (filterId < 0) {
filterId = 0 filterId = 0
} }
return filterId return filterId
} }
export function isSavedFilter(list: IList) { export function isSavedFilter(project: IProject) {
return getSavedFilterIdFromListId(list.id) > 0 return getSavedFilterIdFromProjectId(project.id) > 0
} }
export default class SavedFilterService extends AbstractService<ISavedFilter> { export default class SavedFilterService extends AbstractService<ISavedFilter> {
@ -78,7 +78,7 @@ export default class SavedFilterService extends AbstractService<ISavedFilter> {
} }
} }
export function useSavedFilter(listId?: MaybeRef<IList['id']>) { export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
const router = useRouter() const router = useRouter()
const {t} = useI18n({useScope:'global'}) const {t} = useI18n({useScope:'global'})
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
@ -94,13 +94,13 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
}) })
// load SavedFilter // load SavedFilter
watch(() => unref(listId), async (watchedListId) => { watch(() => unref(projectId), async (watchedProjectId) => {
if (watchedListId === undefined) { if (watchedProjectId === undefined) {
return return
} }
// We assume the listId in the route is the pseudolist // We assume the projectId in the route is the pseudoproject
const savedFilterId = getSavedFilterIdFromListId(watchedListId) const savedFilterId = getSavedFilterIdFromProjectId(watchedProjectId)
filter.value = new SavedFilterModel({id: savedFilterId}) filter.value = new SavedFilterModel({id: savedFilterId})
const response = await filterService.get(filter.value) const response = await filterService.get(filter.value)
@ -111,7 +111,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
async function createFilter() { async function createFilter() {
filter.value = await filterService.create(filter.value) filter.value = await filterService.create(filter.value)
await namespaceStore.loadNamespaces() await namespaceStore.loadNamespaces()
router.push({name: 'list.index', params: {listId: getListId(filter.value)}}) router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}})
} }
async function saveFilter() { async function saveFilter() {
@ -120,8 +120,8 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
success({message: t('filters.edit.success')}) success({message: t('filters.edit.success')})
response.filters = objectToSnakeCase(response.filters) response.filters = objectToSnakeCase(response.filters)
filter.value = response filter.value = response
await useBaseStore().setCurrentList(new ListModel({ await useBaseStore().setCurrentProject(new ProjectModel({
id: getListId(filter.value), id: getProjectId(filter.value),
title: filter.value.title, title: filter.value.title,
})) }))
router.back() router.back()

View File

@ -18,7 +18,7 @@ const parseDate = date => {
export default class TaskService extends AbstractService<ITask> { export default class TaskService extends AbstractService<ITask> {
constructor() { constructor() {
super({ super({
create: '/lists/{listId}', create: '/projects/{projectId}',
getAll: '/tasks/all', getAll: '/tasks/all',
get: '/tasks/{id}', get: '/tasks/{id}',
update: '/tasks/{id}', update: '/tasks/{id}',
@ -43,8 +43,8 @@ export default class TaskService extends AbstractService<ITask> {
model.title = model.title?.trim() model.title = model.title?.trim()
// Ensure that listId is an int // Ensure that projectId is an int
model.listId = Number(model.listId) model.projectId = Number(model.projectId)
// Convert dates into an iso string // Convert dates into an iso string
model.dueDate = parseDate(model.dueDate) model.dueDate = parseDate(model.dueDate)

View File

@ -17,7 +17,7 @@ export interface GetAllTasksParams {
export default class TaskCollectionService extends AbstractService<ITask> { export default class TaskCollectionService extends AbstractService<ITask> {
constructor() { constructor() {
super({ super({
getAll: '/lists/{listId}/tasks', getAll: '/projects/{projectId}/tasks',
}) })
} }

View File

@ -1,20 +1,20 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import TeamListModel from '@/models/teamList' import TeamProjectModel from '@/models/teamProject'
import type {ITeamList} from '@/modelTypes/ITeamList' import type {ITeamProject} from '@/modelTypes/ITeamProject'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
export default class TeamListService extends AbstractService<ITeamList> { export default class TeamProjectService extends AbstractService<ITeamProject> {
constructor() { constructor() {
super({ super({
create: '/lists/{listId}/teams', create: '/projects/{projectId}/teams',
getAll: '/lists/{listId}/teams', getAll: '/projects/{projectId}/teams',
update: '/lists/{listId}/teams/{teamId}', update: '/projects/{projectId}/teams/{teamId}',
delete: '/lists/{listId}/teams/{teamId}', delete: '/projects/{projectId}/teams/{teamId}',
}) })
} }
modelFactory(data) { modelFactory(data) {
return new TeamListModel(data) return new TeamProjectModel(data)
} }
modelGetAllFactory(data) { modelGetAllFactory(data) {

View File

@ -1,20 +1,20 @@
import AbstractService from './abstractService' import AbstractService from './abstractService'
import UserListModel from '@/models/userList' import UserProjectModel from '@/models/userProject'
import type {IUserList} from '@/modelTypes/IUserList' import type {IUserProject} from '@/modelTypes/IUserProject'
import UserModel from '@/models/user' import UserModel from '@/models/user'
export default class UserListService extends AbstractService<IUserList> { export default class UserProjectService extends AbstractService<IUserProject> {
constructor() { constructor() {
super({ super({
create: '/lists/{listId}/users', create: '/projects/{projectId}/users',
getAll: '/lists/{listId}/users', getAll: '/projects/{projectId}/users',
update: '/lists/{listId}/users/{userId}', update: '/projects/{projectId}/users/{userId}',
delete: '/lists/{listId}/users/{userId}', delete: '/projects/{projectId}/users/{userId}',
}) })
} }
modelFactory(data) { modelFactory(data) {
return new UserListModel(data) return new UserProjectModel(data)
} }
modelGetAllFactory(data) { modelGetAllFactory(data) {

View File

@ -3,21 +3,21 @@ import {defineStore, acceptHMRUpdate} from 'pinia'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import ListService from '../services/list' import ProjectService from '../services/project'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl' import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import {useMenuActive} from '@/composables/useMenuActive' import {useMenuActive} from '@/composables/useMenuActive'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
export const useBaseStore = defineStore('base', () => { export const useBaseStore = defineStore('base', () => {
const loading = ref(false) const loading = ref(false)
const ready = ref(false) const ready = ref(false)
// This is used to highlight the current list in menu for all list related views // This is used to highlight the current project in menu for all project related views
const currentList = ref<IList | null>(new ListModel({ const currentProject = ref<IProject | null>(new ProjectModel({
id: 0, id: 0,
isArchived: false, isArchived: false,
})) }))
@ -33,21 +33,21 @@ export const useBaseStore = defineStore('base', () => {
loading.value = newLoading loading.value = newLoading
} }
function setCurrentList(newCurrentList: IList | null) { function setCurrentProject(newCurrentProject: IProject | null) {
// Server updates don't return the right. Therefore, the right is reset after updating the list which is // Server updates don't return the right. Therefore, the right is reset after updating the project which is
// confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right // confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right
// when updating the list in global state. // when updating the project in global state.
if ( if (
typeof currentList.value?.maxRight !== 'undefined' && typeof currentProject.value?.maxRight !== 'undefined' &&
newCurrentList !== null && newCurrentProject !== null &&
( (
typeof newCurrentList.maxRight === 'undefined' || typeof newCurrentProject.maxRight === 'undefined' ||
newCurrentList.maxRight === null newCurrentProject.maxRight === null
) )
) { ) {
newCurrentList.maxRight = currentList.value.maxRight newCurrentProject.maxRight = currentProject.value.maxRight
} }
currentList.value = newCurrentList currentProject.value = newCurrentProject
} }
function setHasTasks(newHasTasks: boolean) { function setHasTasks(newHasTasks: boolean) {
@ -78,44 +78,44 @@ export const useBaseStore = defineStore('base', () => {
ready.value = value ready.value = value
} }
async function handleSetCurrentList( async function handleSetCurrentProject(
{list, forceUpdate = false}: {list: IList | null, forceUpdate?: boolean}, {project, forceUpdate = false}: {project: IProject | null, forceUpdate?: boolean},
) { ) {
if (list === null) { if (project === null) {
setCurrentList({}) setCurrentProject({})
setBackground('') setBackground('')
setBlurHash('') setBlurHash('')
return return
} }
// The forceUpdate parameter is used only when updating a list background directly because in that case // The forceUpdate parameter is used only when updating a project background directly because in that case
// the current list stays the same, but we want to show the new background right away. // the current project stays the same, but we want to show the new background right away.
if (list.id !== currentList.value?.id || forceUpdate) { if (project.id !== currentProject.value?.id || forceUpdate) {
if (list.backgroundInformation) { if (project.backgroundInformation) {
try { try {
const blurHash = await getBlobFromBlurHash(list.backgroundBlurHash) const blurHash = await getBlobFromBlurHash(project.backgroundBlurHash)
if (blurHash) { if (blurHash) {
setBlurHash(window.URL.createObjectURL(blurHash)) setBlurHash(window.URL.createObjectURL(blurHash))
} }
const listService = new ListService() const projectService = new ProjectService()
const background = await listService.background(list) const background = await projectService.background(project)
setBackground(background) setBackground(background)
} catch (e) { } catch (e) {
console.error('Error getting background image for list', list.id, e) console.error('Error getting background image for project', project.id, e)
} }
} }
} }
if ( if (
typeof list.backgroundInformation === 'undefined' || typeof project.backgroundInformation === 'undefined' ||
list.backgroundInformation === null project.backgroundInformation === null
) { ) {
setBackground('') setBackground('')
setBlurHash('') setBlurHash('')
} }
setCurrentList(list) setCurrentProject(project)
} }
const authStore = useAuthStore() const authStore = useAuthStore()
@ -128,7 +128,7 @@ export const useBaseStore = defineStore('base', () => {
return { return {
loading: readonly(loading), loading: readonly(loading),
ready: readonly(ready), ready: readonly(ready),
currentList: readonly(currentList), currentProject: readonly(currentProject),
background: readonly(background), background: readonly(background),
blurHash: readonly(blurHash), blurHash: readonly(blurHash),
hasTasks: readonly(hasTasks), hasTasks: readonly(hasTasks),
@ -138,7 +138,7 @@ export const useBaseStore = defineStore('base', () => {
setLoading, setLoading,
setReady, setReady,
setCurrentList, setCurrentProject,
setHasTasks, setHasTasks,
setKeyboardShortcutsActive, setKeyboardShortcutsActive,
setQuickActionsActive, setQuickActionsActive,
@ -146,7 +146,7 @@ export const useBaseStore = defineStore('base', () => {
setBlurHash, setBlurHash,
setLogoVisible, setLogoVisible,
handleSetCurrentList, handleSetCurrentProject,
loadApp, loadApp,
...useMenuActive(), ...useMenuActive(),

View File

@ -12,7 +12,7 @@ import TaskCollectionService from '@/services/taskCollection'
import {setModuleLoading} from '@/stores/helper' import {setModuleLoading} from '@/stores/helper'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import type {IBucket} from '@/modelTypes/IBucket' import type {IBucket} from '@/modelTypes/IBucket'
const TASKS_PER_BUCKET = 25 const TASKS_PER_BUCKET = 25
@ -45,7 +45,7 @@ const addTaskToBucketAndSort = (buckets: IBucket[], task: ITask) => {
*/ */
export const useKanbanStore = defineStore('kanban', () => { export const useKanbanStore = defineStore('kanban', () => {
const buckets = ref<IBucket[]>([]) const buckets = ref<IBucket[]>([])
const listId = ref<IList['id']>(0) const projectId = ref<IProject['id']>(0)
const bucketLoading = ref<{[id: IBucket['id']]: boolean}>({}) const bucketLoading = ref<{[id: IBucket['id']]: boolean}>({})
const taskPagesPerBucket = ref<{[id: IBucket['id']]: number}>({}) const taskPagesPerBucket = ref<{[id: IBucket['id']]: number}>({})
const allTasksLoadedForBucket = ref<{[id: IBucket['id']]: boolean}>({}) const allTasksLoadedForBucket = ref<{[id: IBucket['id']]: boolean}>({})
@ -68,8 +68,8 @@ export const useKanbanStore = defineStore('kanban', () => {
isLoading.value = newIsLoading isLoading.value = newIsLoading
} }
function setListId(newListId: IList['id']) { function setProjectId(newProjectId: IProject['id']) {
listId.value = Number(newListId) projectId.value = Number(newProjectId)
} }
function setBuckets(newBuckets: IBucket[]) { function setBuckets(newBuckets: IBucket[]) {
@ -223,20 +223,20 @@ export const useKanbanStore = defineStore('kanban', () => {
allTasksLoadedForBucket.value[bucketId] = true allTasksLoadedForBucket.value[bucketId] = true
} }
async function loadBucketsForList({listId, params}: {listId: IList['id'], params}) { async function loadBucketsForProject({projectId, params}: {projectId: IProject['id'], params}) {
const cancel = setModuleLoading(setIsLoading) const cancel = setModuleLoading(setIsLoading)
// Clear everything to prevent having old buckets in the list if loading the buckets from this list 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 bucketService = new BucketService()
try { try {
const newBuckets = await bucketService.getAll({listId}, { const newBuckets = await bucketService.getAll({projectId}, {
...params, ...params,
per_page: TASKS_PER_BUCKET, per_page: TASKS_PER_BUCKET,
}) })
setBuckets(newBuckets) setBuckets(newBuckets)
setListId(listId) setProjectId(projectId)
return newBuckets return newBuckets
} finally { } finally {
cancel() cancel()
@ -244,8 +244,8 @@ export const useKanbanStore = defineStore('kanban', () => {
} }
async function loadNextTasksForBucket( async function loadNextTasksForBucket(
{listId, ps = {}, bucketId} : {projectId, ps = {}, bucketId} :
{listId: IList['id'], ps, bucketId: IBucket['id']}, {projectId: IProject['id'], ps, bucketId: IBucket['id']},
) { ) {
const isLoading = bucketLoading.value[bucketId] ?? false const isLoading = bucketLoading.value[bucketId] ?? false
if (isLoading) { if (isLoading) {
@ -288,7 +288,7 @@ export const useKanbanStore = defineStore('kanban', () => {
const taskService = new TaskCollectionService() const taskService = new TaskCollectionService()
try { try {
const tasks = await taskService.getAll({listId}, params, page) const tasks = await taskService.getAll({projectId}, params, page)
addTasksToBucket({tasks, bucketId: bucketId}) addTasksToBucket({tasks, bucketId: bucketId})
setTasksLoadedForBucketPage({bucketId, page}) setTasksLoadedForBucketPage({bucketId, page})
if (taskService.totalPages <= page) { if (taskService.totalPages <= page) {
@ -322,7 +322,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
loadBucketsForList({listId: bucket.listId, params}) loadBucketsForProject({projectId: bucket.projectId, params})
return response return response
} finally { } finally {
cancel() cancel()
@ -366,7 +366,7 @@ export const useKanbanStore = defineStore('kanban', () => {
} }
await updateBucket({ id, title }) await updateBucket({ id, title })
success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')}) success({message: i18n.global.t('project.kanban.bucketTitleSavedSuccess')})
} }
return { return {
@ -382,7 +382,7 @@ export const useKanbanStore = defineStore('kanban', () => {
setTaskInBucket, setTaskInBucket,
addTaskToBucket, addTaskToBucket,
removeTaskInBucket, removeTaskInBucket,
loadBucketsForList, loadBucketsForProject,
loadNextTasksForBucket, loadNextTasksForBucket,
createBucket, createBucket,
deleteBucket, deleteBucket,

View File

@ -35,7 +35,7 @@ export const useLabelStore = defineStore('label', () => {
}) })
// ** // **
// * Checks if a list of labels is available in the store and filters them then query // * Checks if a project of labels is available in the store and filters them then query
// ** // **
const filterLabelsByQuery = computed(() => { const filterLabelsByQuery = computed(() => {
return (labelsToHide: ILabel[], query: string) => { return (labelsToHide: ILabel[], query: string) => {

View File

@ -5,29 +5,29 @@ import NamespaceService from '../services/namespace'
import {setModuleLoading} from '@/stores/helper' import {setModuleLoading} from '@/stores/helper'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import {useListStore} from '@/stores/lists' import {useProjectStore} from '@/stores/projects'
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description']) const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
export const useNamespaceStore = defineStore('namespace', () => { export const useNamespaceStore = defineStore('namespace', () => {
const listStore = useListStore() const projectStore = useProjectStore()
const isLoading = ref(false) const isLoading = ref(false)
// FIXME: should be object with id as key // FIXME: should be object with id as key
const namespaces = ref<INamespace[]>([]) const namespaces = ref<INamespace[]>([])
const getListAndNamespaceById = computed(() => (listId: IList['id'], ignorePseudoNamespaces = false) => { const getProjectAndNamespaceById = computed(() => (projectId: IProject['id'], ignorePseudoNamespaces = false) => {
for (const n in namespaces.value) { for (const n in namespaces.value) {
if (ignorePseudoNamespaces && namespaces.value[n].id < 0) { if (ignorePseudoNamespaces && namespaces.value[n].id < 0) {
continue continue
} }
for (const l in namespaces.value[n].lists) { for (const l in namespaces.value[n].projects) {
if (namespaces.value[n].lists[l].id === listId) { if (namespaces.value[n].projects[l].id === projectId) {
return { return {
list: namespaces.value[n].lists[l], project: namespaces.value[n].projects[l],
namespace: namespaces.value[n], namespace: namespaces.value[n],
} }
} }
@ -60,9 +60,9 @@ export const useNamespaceStore = defineStore('namespace', () => {
newNamespaces.forEach(n => { newNamespaces.forEach(n => {
add(n) add(n)
// Check for each list in that namespace if it has a subscription and set it if not // Check for each project in that namespace if it has a subscription and set it if not
n.lists.forEach(l => { n.projects.forEach(l => {
if (l.subscription === null || l.subscription.entity !== 'list') { if (l.subscription === null || l.subscription.entity !== 'project') {
l.subscription = n.subscription l.subscription = n.subscription
} }
}) })
@ -76,13 +76,13 @@ export const useNamespaceStore = defineStore('namespace', () => {
return return
} }
if (!namespace.lists || namespace.lists.length === 0) { if (!namespace.projects || namespace.projects.length === 0) {
namespace.lists = namespaces.value[namespaceIndex].lists namespace.projects = namespaces.value[namespaceIndex].projects
} }
// Check for each list in that namespace if it has a subscription and set it if not // Check for each project in that namespace if it has a subscription and set it if not
namespace.lists.forEach(l => { namespace.projects.forEach(l => {
if (l.subscription === null || l.subscription.entity !== 'list') { if (l.subscription === null || l.subscription.entity !== 'project') {
l.subscription = namespace.subscription l.subscription = namespace.subscription
} }
}) })
@ -91,15 +91,15 @@ export const useNamespaceStore = defineStore('namespace', () => {
update(namespace) update(namespace)
} }
function setListInNamespaceById(list: IList) { function setProjectInNamespaceById(project: IProject) {
for (const n in namespaces.value) { for (const n in namespaces.value) {
// We don't have the namespace id on the list which means we need to loop over all lists until we find it. // We don't have the namespace id on the project which means we need to loop over all projects until we find it.
// FIXME: Not ideal at all - we should fix that at the api level. // FIXME: Not ideal at all - we should fix that at the api level.
if (namespaces.value[n].id === list.namespaceId) { if (namespaces.value[n].id === project.namespaceId) {
for (const l in namespaces.value[n].lists) { for (const l in namespaces.value[n].projects) {
if (namespaces.value[n].lists[l].id === list.id) { if (namespaces.value[n].projects[l].id === project.id) {
const namespace = namespaces.value[n] const namespace = namespaces.value[n]
namespace.lists[l] = list namespace.projects[l] = project
namespaces.value[n] = namespace namespaces.value[n] = namespace
return return
} }
@ -123,23 +123,23 @@ export const useNamespaceStore = defineStore('namespace', () => {
} }
} }
function addListToNamespace(list: IList) { function addProjectToNamespace(project: IProject) {
for (const n in namespaces.value) { for (const n in namespaces.value) {
if (namespaces.value[n].id === list.namespaceId) { if (namespaces.value[n].id === project.namespaceId) {
namespaces.value[n].lists.push(list) namespaces.value[n].projects.push(project)
return return
} }
} }
} }
function removeListFromNamespaceById(list: IList) { function removeProjectFromNamespaceById(project: IProject) {
for (const n in namespaces.value) { for (const n in namespaces.value) {
// We don't have the namespace id on the list which means we need to loop over all lists until we find it. // We don't have the namespace id on the project which means we need to loop over all projects until we find it.
// FIXME: Not ideal at all - we should fix that at the api level. // FIXME: Not ideal at all - we should fix that at the api level.
if (namespaces.value[n].id === list.namespaceId) { if (namespaces.value[n].id === project.namespaceId) {
for (const l in namespaces.value[n].lists) { for (const l in namespaces.value[n].projects) {
if (namespaces.value[n].lists[l].id === list.id) { if (namespaces.value[n].projects[l].id === project.id) {
namespaces.value[n].lists.splice(l, 1) namespaces.value[n].projects.splice(l, 1)
return return
} }
} }
@ -156,10 +156,10 @@ export const useNamespaceStore = defineStore('namespace', () => {
const namespaces = await namespaceService.getAll({}, {is_archived: true}) as INamespace[] const namespaces = await namespaceService.getAll({}, {is_archived: true}) as INamespace[]
setNamespaces(namespaces) setNamespaces(namespaces)
// Put all lists in the list state // Put all projects in the project state
const lists = namespaces.flatMap(({lists}) => lists) const projects = namespaces.flatMap(({projects}) => projects)
listStore.setLists(lists) projectStore.setProjects(projects)
return namespaces return namespaces
} finally { } finally {
@ -176,7 +176,7 @@ export const useNamespaceStore = defineStore('namespace', () => {
} }
function removeFavoritesNamespaceIfEmpty() { function removeFavoritesNamespaceIfEmpty() {
if (namespaces.value[0].id === -2 && namespaces.value[0].lists.length === 0) { if (namespaces.value[0].id === -2 && namespaces.value[0].projects.length === 0) {
namespaces.value.splice(0, 1) namespaces.value.splice(0, 1)
} }
} }
@ -211,17 +211,17 @@ export const useNamespaceStore = defineStore('namespace', () => {
isLoading: readonly(isLoading), isLoading: readonly(isLoading),
namespaces: readonly(namespaces), namespaces: readonly(namespaces),
getListAndNamespaceById, getProjectAndNamespaceById,
getNamespaceById, getNamespaceById,
searchNamespace, searchNamespace,
setNamespaces, setNamespaces,
setNamespaceById, setNamespaceById,
setListInNamespaceById, setProjectInNamespaceById,
addNamespace, addNamespace,
removeNamespaceById, removeNamespaceById,
addListToNamespace, addProjectToNamespace,
removeListFromNamespaceById, removeProjectFromNamespaceById,
loadNamespaces, loadNamespaces,
loadNamespacesIfFavoritesDontExist, loadNamespacesIfFavoritesDontExist,
removeFavoritesNamespaceIfEmpty, removeFavoritesNamespaceIfEmpty,

View File

@ -2,57 +2,57 @@ import {watch, reactive, shallowReactive, unref, toRefs, readonly, ref, computed
import {acceptHMRUpdate, defineStore} from 'pinia' import {acceptHMRUpdate, defineStore} from 'pinia'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import ListService from '@/services/list' import ProjectService from '@/services/project'
import {setModuleLoading} from '@/stores/helper' import {setModuleLoading} from '@/stores/helper'
import {removeListFromHistory} from '@/modules/listHistory' import {removeProjectFromHistory} from '@/modules/projectHistory'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import {useNamespaceStore} from './namespaces' import {useNamespaceStore} from './namespaces'
import type {IList} from '@/modelTypes/IList' import type {IProject} from '@/modelTypes/IProject'
import type {MaybeRef} from '@vueuse/core' import type {MaybeRef} from '@vueuse/core'
import ListModel from '@/models/list' import ProjectModel from '@/models/project'
import {success} from '@/message' import {success} from '@/message'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description']) const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
const FavoriteListsNamespace = -2 const FavoriteProjectsNamespace = -2
export interface ListState { export interface ProjectState {
[id: IList['id']]: IList [id: IProject['id']]: IProject
} }
export const useListStore = defineStore('list', () => { export const useProjectStore = defineStore('project', () => {
const baseStore = useBaseStore() const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const isLoading = ref(false) const isLoading = ref(false)
// The lists are stored as an object which has the list ids as keys. // The projects are stored as an object which has the project ids as keys.
const lists = ref<ListState>({}) const projects = ref<ProjectState>({})
const getListById = computed(() => { const getProjectById = computed(() => {
return (id: IList['id']) => typeof lists.value[id] !== 'undefined' ? lists.value[id] : null return (id: IProject['id']) => typeof projects.value[id] !== 'undefined' ? projects.value[id] : null
}) })
const findListByExactname = computed(() => { const findProjectByExactname = computed(() => {
return (name: string) => { return (name: string) => {
const list = Object.values(lists.value).find(l => { const project = Object.values(projects.value).find(l => {
return l.title.toLowerCase() === name.toLowerCase() return l.title.toLowerCase() === name.toLowerCase()
}) })
return typeof list === 'undefined' ? null : list return typeof project === 'undefined' ? null : project
} }
}) })
const searchList = computed(() => { const searchProject = computed(() => {
return (query: string, includeArchived = false) => { return (query: string, includeArchived = false) => {
return search(query) return search(query)
?.filter(value => value > 0) ?.filter(value => value > 0)
.map(id => lists.value[id]) .map(id => projects.value[id])
.filter(list => list.isArchived === includeArchived) .filter(project => project.isArchived === includeArchived)
|| [] || []
} }
}) })
@ -61,82 +61,82 @@ export const useListStore = defineStore('list', () => {
isLoading.value = newIsLoading isLoading.value = newIsLoading
} }
function setList(list: IList) { function setProject(project: IProject) {
lists.value[list.id] = list projects.value[project.id] = project
update(list) update(project)
if (baseStore.currentList?.id === list.id) { if (baseStore.currentProject?.id === project.id) {
baseStore.setCurrentList(list) baseStore.setCurrentProject(project)
} }
} }
function setLists(newLists: IList[]) { function setProjects(newProjects: IProject[]) {
newLists.forEach(l => { newProjects.forEach(l => {
lists.value[l.id] = l projects.value[l.id] = l
add(l) add(l)
}) })
} }
function removeListById(list: IList) { function removeProjectById(project: IProject) {
remove(list) remove(project)
delete lists.value[list.id] delete projects.value[project.id]
} }
function toggleListFavorite(list: IList) { function toggleProjectFavorite(project: IProject) {
// The favorites pseudo list is always favorite // The favorites pseudo project is always favorite
// Archived lists cannot be marked favorite // Archived projects cannot be marked favorite
if (list.id === -1 || list.isArchived) { if (project.id === -1 || project.isArchived) {
return return
} }
return updateList({ return updateProject({
...list, ...project,
isFavorite: !list.isFavorite, isFavorite: !project.isFavorite,
}) })
} }
async function createList(list: IList) { async function createProject(project: IProject) {
const cancel = setModuleLoading(setIsLoading) const cancel = setModuleLoading(setIsLoading)
const listService = new ListService() const projectService = new ProjectService()
try { try {
const createdList = await listService.create(list) const createdProject = await projectService.create(project)
createdList.namespaceId = list.namespaceId createdProject.namespaceId = project.namespaceId
namespaceStore.addListToNamespace(createdList) namespaceStore.addProjectToNamespace(createdProject)
setList(createdList) setProject(createdProject)
return createdList return createdProject
} finally { } finally {
cancel() cancel()
} }
} }
async function updateList(list: IList) { async function updateProject(project: IProject) {
const cancel = setModuleLoading(setIsLoading) const cancel = setModuleLoading(setIsLoading)
const listService = new ListService() const projectService = new ProjectService()
try { try {
await listService.update(list) await projectService.update(project)
setList(list) setProject(project)
namespaceStore.setListInNamespaceById(list) namespaceStore.setProjectInNamespaceById(project)
// the returned list from listService.update is the same! // the returned project from projectService.update is the same!
// in order to not create a manipulation in pinia store we have to create a new copy // in order to not create a manipulation in pinia store we have to create a new copy
const newList = { const newProject = {
...list, ...project,
namespaceId: FavoriteListsNamespace, namespaceId: FavoriteProjectsNamespace,
} }
namespaceStore.removeListFromNamespaceById(newList) namespaceStore.removeProjectFromNamespaceById(newProject)
if (list.isFavorite) { if (project.isFavorite) {
namespaceStore.addListToNamespace(newList) namespaceStore.addProjectToNamespace(newProject)
} }
namespaceStore.loadNamespacesIfFavoritesDontExist() namespaceStore.loadNamespacesIfFavoritesDontExist()
namespaceStore.removeFavoritesNamespaceIfEmpty() namespaceStore.removeFavoritesNamespaceIfEmpty()
return newList return newProject
} catch (e) { } catch (e) {
// Reset the list state to the initial one to avoid confusion for the user // Reset the project state to the initial one to avoid confusion for the user
setList({ setProject({
...list, ...project,
isFavorite: !list.isFavorite, isFavorite: !project.isFavorite,
}) })
throw e throw e
} finally { } finally {
@ -144,15 +144,15 @@ export const useListStore = defineStore('list', () => {
} }
} }
async function deleteList(list: IList) { async function deleteProject(project: IProject) {
const cancel = setModuleLoading(setIsLoading) const cancel = setModuleLoading(setIsLoading)
const listService = new ListService() const projectService = new ProjectService()
try { try {
const response = await listService.delete(list) const response = await projectService.delete(project)
removeListById(list) removeProjectById(project)
namespaceStore.removeListFromNamespaceById(list) namespaceStore.removeProjectFromNamespaceById(project)
removeListFromHistory({id: list.id}) removeProjectFromHistory({id: project.id})
return response return response
} finally { } finally {
cancel() cancel()
@ -161,51 +161,51 @@ export const useListStore = defineStore('list', () => {
return { return {
isLoading: readonly(isLoading), isLoading: readonly(isLoading),
lists: readonly(lists), projects: readonly(projects),
getListById, getProjectById,
findListByExactname, findProjectByExactname,
searchList, searchProject,
setList, setProject,
setLists, setProjects,
removeListById, removeProjectById,
toggleListFavorite, toggleProjectFavorite,
createList, createProject,
updateList, updateProject,
deleteList, deleteProject,
} }
}) })
export function useList(listId: MaybeRef<IList['id']>) { export function useProject(projectId: MaybeRef<IProject['id']>) {
const listService = shallowReactive(new ListService()) const projectService = shallowReactive(new ProjectService())
const {loading: isLoading} = toRefs(listService) const {loading: isLoading} = toRefs(projectService)
const list: IList = reactive(new ListModel()) const project: IProject = reactive(new ProjectModel())
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
watch( watch(
() => unref(listId), () => unref(projectId),
async (listId) => { async (projectId) => {
const loadedList = await listService.get(new ListModel({id: listId})) const loadedProject = await projectService.get(new ProjectModel({id: projectId}))
Object.assign(list, loadedList) Object.assign(project, loadedProject)
}, },
{immediate: true}, {immediate: true},
) )
const listStore = useListStore() const projectStore = useProjectStore()
async function save() { async function save() {
await listStore.updateList(list) await projectStore.updateProject(project)
success({message: t('list.edit.success')}) success({message: t('project.edit.success')})
} }
return { return {
isLoading: readonly(isLoading), isLoading: readonly(isLoading),
list, project,
save, save,
} }
} }
// support hot reloading // support hot reloading
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useListStore, import.meta.hot)) import.meta.hot.accept(acceptHMRUpdate(useProjectStore, import.meta.hot))
} }

Some files were not shown because too many files have changed in this diff Show More