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:
parent
b9d3b5c756
commit
befa6f27bb
@ -1,18 +1,18 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {UserListFactory} from '../../factories/users_list'
|
||||
import {UserProjectFactory} from '../../factories/users_project'
|
||||
|
||||
describe('Editor', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
beforeEach(() => {
|
||||
NamespaceFactory.create(1)
|
||||
ListFactory.create(1)
|
||||
ProjectFactory.create(1)
|
||||
TaskFactory.truncate()
|
||||
UserListFactory.truncate()
|
||||
UserProjectFactory.truncate()
|
||||
})
|
||||
|
||||
it('Has a preview with checkable checkboxes', () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
|
||||
describe('Namepaces', () => {
|
||||
@ -10,7 +10,7 @@ describe('Namepaces', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
namespaces = NamespaceFactory.create(1)
|
||||
ListFactory.create(1)
|
||||
ProjectFactory.create(1)
|
||||
})
|
||||
|
||||
it('Should be all there', () => {
|
||||
@ -99,17 +99,17 @@ describe('Namepaces', () => {
|
||||
.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, {
|
||||
id: 2,
|
||||
is_archived: true,
|
||||
}, false)
|
||||
ListFactory.create(1, {
|
||||
ProjectFactory.create(1, {
|
||||
id: 2,
|
||||
namespace_id: n[0].id,
|
||||
}, false)
|
||||
|
||||
ListFactory.create(1, {
|
||||
ProjectFactory.create(1, {
|
||||
id: 3,
|
||||
is_archived: true,
|
||||
}, false)
|
@ -1,19 +1,19 @@
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
|
||||
export function createLists() {
|
||||
export function createProjects() {
|
||||
NamespaceFactory.create(1)
|
||||
const lists = ListFactory.create(1, {
|
||||
title: 'First List'
|
||||
const projects = ProjectFactory.create(1, {
|
||||
title: 'First Project'
|
||||
})
|
||||
TaskFactory.truncate()
|
||||
return lists
|
||||
return projects
|
||||
}
|
||||
|
||||
export function prepareLists(setLists = (...args: any[]) => {}) {
|
||||
export function prepareProjects(setProjects = (...args: any[]) => {}) {
|
||||
beforeEach(() => {
|
||||
const lists = createLists()
|
||||
setLists(lists)
|
||||
const projects = createProjects()
|
||||
setProjects(projects)
|
||||
})
|
||||
}
|
@ -1,41 +1,41 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {prepareLists} from './prepareLists'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {prepareProjects} from './prepareProjects'
|
||||
|
||||
describe('List History', () => {
|
||||
describe('Project History', () => {
|
||||
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') + '/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.wait('@loadNamespaces')
|
||||
cy.get('body')
|
||||
.should('not.contain', 'Last viewed')
|
||||
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
cy.visit(`/projects/${projects[0].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[1].id}`)
|
||||
cy.wait('@loadProject')
|
||||
cy.visit(`/projects/${projects[1].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[2].id}`)
|
||||
cy.wait('@loadProject')
|
||||
cy.visit(`/projects/${projects[2].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[3].id}`)
|
||||
cy.wait('@loadProject')
|
||||
cy.visit(`/projects/${projects[3].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[4].id}`)
|
||||
cy.wait('@loadProject')
|
||||
cy.visit(`/projects/${projects[4].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[5].id}`)
|
||||
cy.wait('@loadProject')
|
||||
cy.visit(`/projects/${projects[5].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.wait('@loadProject')
|
||||
|
||||
// cy.visit('/')
|
||||
// cy.wait('@loadNamespaces')
|
||||
@ -46,12 +46,12 @@ describe('List History', () => {
|
||||
|
||||
cy.get('body')
|
||||
.should('contain', 'Last viewed')
|
||||
cy.get('[data-cy="listCardGrid"]')
|
||||
.should('not.contain', lists[0].title)
|
||||
.should('contain', lists[1].title)
|
||||
.should('contain', lists[2].title)
|
||||
.should('contain', lists[3].title)
|
||||
.should('contain', lists[4].title)
|
||||
.should('contain', lists[5].title)
|
||||
cy.get('[data-cy="projectCardGrid"]')
|
||||
.should('not.contain', projects[0].title)
|
||||
.should('contain', projects[1].title)
|
||||
.should('contain', projects[2].title)
|
||||
.should('contain', projects[3].title)
|
||||
.should('contain', projects[4].title)
|
||||
.should('contain', projects[5].title)
|
||||
})
|
||||
})
|
@ -3,15 +3,15 @@ import {formatISO, format} from 'date-fns'
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {prepareLists} from './prepareLists'
|
||||
import {prepareProjects} from './prepareProjects'
|
||||
|
||||
describe('List View Gantt', () => {
|
||||
describe('Project View Gantt', () => {
|
||||
createFakeUserAndLogin()
|
||||
prepareLists()
|
||||
prepareProjects()
|
||||
|
||||
it('Hides tasks with no dates', () => {
|
||||
const tasks = TaskFactory.create(1)
|
||||
cy.visit('/lists/1/gantt')
|
||||
cy.visit('/projects/1/gantt')
|
||||
|
||||
cy.get('.g-gantt-rows-container')
|
||||
.should('not.contain', tasks[0].title)
|
||||
@ -25,7 +25,7 @@ describe('List View Gantt', () => {
|
||||
nextMonth.setDate(1)
|
||||
nextMonth.setMonth(9)
|
||||
|
||||
cy.visit('/lists/1/gantt')
|
||||
cy.visit('/projects/1/gantt')
|
||||
|
||||
cy.get('.g-timeunits-container')
|
||||
.should('contain', format(now, 'MMMM'))
|
||||
@ -38,7 +38,7 @@ describe('List View Gantt', () => {
|
||||
start_date: now.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')
|
||||
.should('not.be.empty')
|
||||
@ -50,7 +50,7 @@ describe('List View Gantt', () => {
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
cy.visit('/projects/1/gantt')
|
||||
|
||||
cy.get('.gantt-options .fancycheckbox')
|
||||
.contains('Show tasks which don\'t have dates set')
|
||||
@ -69,7 +69,7 @@ describe('List View Gantt', () => {
|
||||
start_date: now.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')
|
||||
.first()
|
||||
@ -83,9 +83,9 @@ describe('List View Gantt', () => {
|
||||
const now = Date.UTC(2022, 10, 9)
|
||||
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()
|
||||
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
|
||||
.first()
|
||||
@ -99,13 +99,13 @@ describe('List View Gantt', () => {
|
||||
})
|
||||
|
||||
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')
|
||||
.should('contain', 'September 2022')
|
||||
.should('contain', 'October 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')
|
||||
})
|
||||
|
||||
@ -115,7 +115,7 @@ describe('List View Gantt', () => {
|
||||
start_date: formatISO(now),
|
||||
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')
|
||||
.dblclick()
|
@ -1,13 +1,13 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {prepareLists} from './prepareLists'
|
||||
import {prepareProjects} from './prepareProjects'
|
||||
|
||||
describe('List View Kanban', () => {
|
||||
describe('Project View Kanban', () => {
|
||||
createFakeUserAndLogin()
|
||||
prepareLists()
|
||||
prepareProjects()
|
||||
|
||||
let buckets
|
||||
beforeEach(() => {
|
||||
@ -16,10 +16,10 @@ describe('List View Kanban', () => {
|
||||
|
||||
it('Shows all buckets with their tasks', () => {
|
||||
const data = TaskFactory.create(10, {
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
cy.visit('/projects/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[0].title)
|
||||
@ -34,10 +34,10 @@ describe('List View Kanban', () => {
|
||||
|
||||
it('Can add a new task to a bucket', () => {
|
||||
TaskFactory.create(2, {
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
cy.visit('/projects/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket')
|
||||
.contains(buckets[0].title)
|
||||
@ -55,7 +55,7 @@ describe('List View Kanban', () => {
|
||||
})
|
||||
|
||||
it('Can create a new bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
cy.visit('/projects/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket.new-bucket .button')
|
||||
.click()
|
||||
@ -69,7 +69,7 @@ describe('List View Kanban', () => {
|
||||
})
|
||||
|
||||
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')
|
||||
.first()
|
||||
@ -90,7 +90,7 @@ describe('List View Kanban', () => {
|
||||
})
|
||||
|
||||
it('Can rename a bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
cy.visit('/projects/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header .title')
|
||||
.first()
|
||||
@ -101,7 +101,7 @@ describe('List View Kanban', () => {
|
||||
})
|
||||
|
||||
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')
|
||||
.first()
|
||||
@ -125,10 +125,10 @@ describe('List View Kanban', () => {
|
||||
|
||||
it('Can drag tasks around', () => {
|
||||
const tasks = TaskFactory.create(2, {
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
cy.visit('/projects/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
@ -144,10 +144,10 @@ describe('List View Kanban', () => {
|
||||
it('Should navigate to the task when the task card is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
cy.visit('/projects/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
@ -158,18 +158,18 @@ describe('List View Kanban', () => {
|
||||
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
|
||||
})
|
||||
|
||||
it('Should remove a task from the kanban board when moving it to another list', () => {
|
||||
const lists = ListFactory.create(2)
|
||||
it('Should remove a task from the kanban board when moving it to another project', () => {
|
||||
const projects = ProjectFactory.create(2)
|
||||
BucketFactory.create(2, {
|
||||
list_id: '{increment}',
|
||||
project_id: '{increment}',
|
||||
})
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
const task = tasks[0]
|
||||
cy.visit('/lists/1/kanban')
|
||||
cy.visit('/projects/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
.contains(task.title)
|
||||
@ -180,7 +180,7 @@ describe('List View Kanban', () => {
|
||||
.contains('Move')
|
||||
.click()
|
||||
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
|
||||
// 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')
|
||||
@ -197,26 +197,26 @@ describe('List View Kanban', () => {
|
||||
|
||||
it('Shows a button to filter the kanban board', () => {
|
||||
const data = TaskFactory.create(10, {
|
||||
list_id: 1,
|
||||
project_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')
|
||||
})
|
||||
|
||||
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, {
|
||||
list_id: lists[0].id,
|
||||
project_id: projects[0].id,
|
||||
})
|
||||
const tasks = TaskFactory.create(5, {
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
bucket_id: buckets[0].id,
|
||||
})
|
||||
const task = tasks[0]
|
||||
cy.visit('/lists/1/kanban')
|
||||
cy.visit('/projects/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
.contains(task.title)
|
@ -1,32 +1,32 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {UserListFactory} from '../../factories/users_list'
|
||||
import {UserProjectFactory} from '../../factories/users_project'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {prepareLists} from './prepareLists'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {prepareProjects} from './prepareProjects'
|
||||
|
||||
describe('List View List', () => {
|
||||
describe('Project View Project', () => {
|
||||
createFakeUserAndLogin()
|
||||
prepareLists()
|
||||
prepareProjects()
|
||||
|
||||
it('Should be an empty list', () => {
|
||||
cy.visit('/lists/1')
|
||||
it('Should be an empty project', () => {
|
||||
cy.visit('/projects/1')
|
||||
cy.url()
|
||||
.should('contain', '/lists/1/list')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'First List')
|
||||
cy.get('.list-title-dropdown')
|
||||
.should('contain', '/projects/1/list')
|
||||
cy.get('.project-title')
|
||||
.should('contain', 'First Project')
|
||||
cy.get('.project-title-dropdown')
|
||||
.should('exist')
|
||||
cy.get('p')
|
||||
.contains('This list is currently empty.')
|
||||
.contains('This project is currently empty.')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Should create a new task', () => {
|
||||
const newTaskTitle = 'New task'
|
||||
|
||||
cy.visit('/lists/1')
|
||||
cy.visit('/projects/1')
|
||||
cy.get('.task-add textarea')
|
||||
.type(newTaskTitle+'{enter}')
|
||||
cy.get('.tasks')
|
||||
@ -36,9 +36,9 @@ describe('List View List', () => {
|
||||
it('Should navigate to the task when the title is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/list')
|
||||
cy.visit('/projects/1/list')
|
||||
|
||||
cy.get('.tasks .task .tasktext')
|
||||
.contains(tasks[0].title)
|
||||
@ -49,33 +49,33 @@ describe('List View List', () => {
|
||||
.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)
|
||||
UserListFactory.create(1, {
|
||||
list_id: 2,
|
||||
UserProjectFactory.create(1, {
|
||||
project_id: 2,
|
||||
user_id: 1,
|
||||
right: 0,
|
||||
})
|
||||
const lists = ListFactory.create(2, {
|
||||
const projects = ProjectFactory.create(2, {
|
||||
owner_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')
|
||||
cy.get('input.input[placeholder="Add a new task..."')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Should only show the color of a list in the navigation and not in the list view', () => {
|
||||
const lists = ListFactory.create(1, {
|
||||
it('Should only show the color of a project in the navigation and not in the project view', () => {
|
||||
const projects = ProjectFactory.create(1, {
|
||||
hex_color: '00db60',
|
||||
})
|
||||
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')
|
||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||
@ -87,9 +87,9 @@ describe('List View List', () => {
|
||||
const tasks = TaskFactory.create(100, {
|
||||
id: '{increment}',
|
||||
title: i => `task${i}`,
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/list')
|
||||
cy.visit('/projects/1/list')
|
||||
|
||||
cy.get('.tasks')
|
||||
.should('contain', tasks[1].title)
|
@ -2,37 +2,37 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
|
||||
describe('List View Table', () => {
|
||||
describe('Project View Table', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
it('Should show a table with tasks', () => {
|
||||
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')
|
||||
cy.get('.list-table table.table')
|
||||
cy.get('.project-table table.table')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Should have working column switches', () => {
|
||||
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')
|
||||
.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')
|
||||
.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')
|
||||
.click()
|
||||
|
||||
cy.get('.list-table table.table th')
|
||||
cy.get('.project-table table.table th')
|
||||
.contains('Priority')
|
||||
.should('exist')
|
||||
cy.get('.list-table table.table th')
|
||||
cy.get('.project-table table.table th')
|
||||
.contains('Done')
|
||||
.should('not.exist')
|
||||
})
|
||||
@ -40,11 +40,11 @@ describe('List View Table', () => {
|
||||
it('Should navigate to the task when the title is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
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)
|
||||
.click()
|
||||
|
@ -1,58 +1,58 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {prepareLists} from './prepareLists'
|
||||
import {prepareProjects} from './prepareProjects'
|
||||
|
||||
describe('Lists', () => {
|
||||
describe('Projects', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
let lists
|
||||
prepareLists((newLists) => (lists = newLists))
|
||||
let projects
|
||||
prepareProjects((newProjects) => (projects = newProjects))
|
||||
|
||||
it('Should create a new list', () => {
|
||||
it('Should create a new project', () => {
|
||||
cy.visit('/')
|
||||
cy.get('.namespace-title .dropdown-trigger')
|
||||
.click()
|
||||
cy.get('.namespace-title .dropdown .dropdown-item')
|
||||
.contains('New list')
|
||||
.contains('New project')
|
||||
.click()
|
||||
cy.url()
|
||||
.should('contain', '/lists/new/1')
|
||||
.should('contain', '/projects/new/1')
|
||||
cy.get('.card-header-title')
|
||||
.contains('New list')
|
||||
.contains('New project')
|
||||
cy.get('input.input')
|
||||
.type('New List')
|
||||
.type('New Project')
|
||||
cy.get('.button')
|
||||
.contains('Create')
|
||||
.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')
|
||||
cy.url()
|
||||
.should('contain', '/lists/')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'New List')
|
||||
.should('contain', '/projects/')
|
||||
cy.get('.project-title')
|
||||
.should('contain', 'New Project')
|
||||
})
|
||||
|
||||
it('Should redirect to a specific list view after visited', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
it('Should redirect to a specific project view after visited', () => {
|
||||
cy.visit('/projects/1/kanban')
|
||||
cy.url()
|
||||
.should('contain', '/lists/1/kanban')
|
||||
cy.visit('/lists/1')
|
||||
.should('contain', '/projects/1/kanban')
|
||||
cy.visit('/projects/1')
|
||||
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, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
const newListName = 'New list name'
|
||||
const newProjectName = 'New project name'
|
||||
|
||||
cy.visit('/lists/1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'First List')
|
||||
cy.visit('/projects/1')
|
||||
cy.get('.project-title')
|
||||
.should('contain', 'First Project')
|
||||
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||
.click()
|
||||
@ -60,27 +60,27 @@ describe('Lists', () => {
|
||||
.contains('Edit')
|
||||
.click()
|
||||
cy.get('#title')
|
||||
.type(`{selectall}${newListName}`)
|
||||
.type(`{selectall}${newProjectName}`)
|
||||
cy.get('footer.card-footer .button')
|
||||
.contains('Save')
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.list-title')
|
||||
.should('contain', newListName)
|
||||
.should('not.contain', lists[0].title)
|
||||
cy.get('.project-title')
|
||||
.should('contain', newProjectName)
|
||||
.should('not.contain', projects[0].title)
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
|
||||
.should('contain', newListName)
|
||||
.should('not.contain', lists[0].title)
|
||||
.should('contain', newProjectName)
|
||||
.should('not.contain', projects[0].title)
|
||||
cy.visit('/')
|
||||
cy.get('.card-content')
|
||||
.should('contain', newListName)
|
||||
.should('not.contain', lists[0].title)
|
||||
.should('contain', newProjectName)
|
||||
.should('not.contain', projects[0].title)
|
||||
})
|
||||
|
||||
it('Should remove a list', () => {
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
it('Should remove a project', () => {
|
||||
cy.visit(`/projects/${projects[0].id}`)
|
||||
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||
.click()
|
||||
@ -96,27 +96,27 @@ describe('Lists', () => {
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list')
|
||||
.should('not.contain', lists[0].title)
|
||||
.should('not.contain', projects[0].title)
|
||||
cy.location('pathname')
|
||||
.should('equal', '/')
|
||||
})
|
||||
|
||||
it('Should archive a list', () => {
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
it('Should archive a project', () => {
|
||||
cy.visit(`/projects/${projects[0].id}`)
|
||||
|
||||
cy.get('.list-title-dropdown')
|
||||
cy.get('.project-title-dropdown')
|
||||
.click()
|
||||
cy.get('.list-title-dropdown .dropdown-menu .dropdown-item')
|
||||
cy.get('.project-title-dropdown .dropdown-menu .dropdown-item')
|
||||
.contains('Archive')
|
||||
.click()
|
||||
cy.get('.modal-content')
|
||||
.should('contain.text', 'Archive this list')
|
||||
.should('contain.text', 'Archive this project')
|
||||
cy.get('.modal-content [data-cy=modalPrimary]')
|
||||
.click()
|
||||
|
||||
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')
|
||||
.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.')
|
||||
})
|
||||
})
|
@ -1,22 +1,22 @@
|
||||
import {LinkShareFactory} from '../../factories/link_sharing'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
|
||||
describe('Link shares', () => {
|
||||
it('Can view a link share', () => {
|
||||
const lists = ListFactory.create(1)
|
||||
const projects = ProjectFactory.create(1)
|
||||
const tasks = TaskFactory.create(10, {
|
||||
list_id: lists[0].id
|
||||
project_id: projects[0].id
|
||||
})
|
||||
const linkShares = LinkShareFactory.create(1, {
|
||||
list_id: lists[0].id,
|
||||
project_id: projects[0].id,
|
||||
right: 0,
|
||||
})
|
||||
|
||||
cy.visit(`/share/${linkShares[0].hash}/auth`)
|
||||
|
||||
cy.get('h1.title')
|
||||
.should('contain', lists[0].title)
|
||||
.should('contain', projects[0].title)
|
||||
cy.get('input.input[placeholder="Add a new task..."')
|
||||
.should('not.exist')
|
||||
cy.get('.tasks')
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {seed} from '../../support/seed'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
@ -9,9 +9,9 @@ import {updateUserSettings} from '../../support/updateUserSettings'
|
||||
|
||||
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||
NamespaceFactory.create(1)
|
||||
const list = ListFactory.create()[0]
|
||||
const project = ProjectFactory.create()[0]
|
||||
BucketFactory.create(1, {
|
||||
list_id: list.id,
|
||||
project_id: project.id,
|
||||
})
|
||||
const tasks = []
|
||||
let dueDate = startDueDate
|
||||
@ -20,7 +20,7 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||
dueDate = new Date(new Date(dueDate).setDate(dueDate.getDate() + 2))
|
||||
tasks.push({
|
||||
id: i + 1,
|
||||
list_id: list.id,
|
||||
project_id: project.id,
|
||||
done: false,
|
||||
created_by_id: 1,
|
||||
title: 'Test Task ' + i,
|
||||
@ -31,7 +31,7 @@ function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
|
||||
})
|
||||
}
|
||||
seed(TaskFactory.table, tasks)
|
||||
return {tasks, list}
|
||||
return {tasks, project}
|
||||
}
|
||||
|
||||
describe('Home Page Task Overview', () => {
|
||||
@ -73,7 +73,7 @@ describe('Home Page Task Overview', () => {
|
||||
due_date: new Date().toISOString(),
|
||||
}, false)
|
||||
|
||||
cy.visit(`/lists/${tasks[0].list_id}/list`)
|
||||
cy.visit(`/projects/${tasks[0].project_id}/list`)
|
||||
cy.get('.tasks .task')
|
||||
.first()
|
||||
.should('contain.text', newTaskTitle)
|
||||
@ -90,7 +90,7 @@ describe('Home Page Task Overview', () => {
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
cy.visit(`/lists/${tasks[0].list_id}/list`)
|
||||
cy.visit(`/projects/${tasks[0].project_id}/list`)
|
||||
cy.get('.task-add textarea')
|
||||
.type(newTaskTitle+'{enter}')
|
||||
cy.visit('/')
|
||||
@ -113,10 +113,10 @@ describe('Home Page Task Overview', () => {
|
||||
.should('contain.text', newTaskTitle)
|
||||
})
|
||||
|
||||
it('Should show a task without a due date added via default list at the bottom', () => {
|
||||
const {list} = seedTasks(40)
|
||||
it('Should show a task without a due date added via default project at the bottom', () => {
|
||||
const {project} = seedTasks(40)
|
||||
updateUserSettings({
|
||||
default_list_id: list.id,
|
||||
default_project_id: project.id,
|
||||
overdue_tasks_reminders_time: '9:00',
|
||||
})
|
||||
|
||||
@ -131,23 +131,23 @@ describe('Home Page Task Overview', () => {
|
||||
.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()
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('.home.app-content .content')
|
||||
.should('contain.text', 'You can create a new list for your new tasks:')
|
||||
.should('contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
||||
.should('contain.text', 'You can create a new project for your new tasks:')
|
||||
.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()
|
||||
|
||||
cy.visit('/')
|
||||
|
||||
cy.get('.home.app-content .content')
|
||||
.should('not.contain.text', 'You can create a new list for your new tasks:')
|
||||
.should('not.contain.text', 'Or import your lists and tasks from other services into Vikunja:')
|
||||
.should('not.contain.text', 'You can create a new project for your new tasks:')
|
||||
.should('not.contain.text', 'Or import your projects and tasks from other services into Vikunja:')
|
||||
})
|
||||
})
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {TaskCommentFactory} from '../../factories/task_comment'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {UserListFactory} from '../../factories/users_list'
|
||||
import {UserProjectFactory} from '../../factories/users_project'
|
||||
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
||||
import {LabelFactory} from '../../factories/labels'
|
||||
import {LabelTaskFactory} from '../../factories/label_task'
|
||||
@ -48,22 +48,22 @@ describe('Task', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
let namespaces
|
||||
let lists
|
||||
let projects
|
||||
let buckets
|
||||
|
||||
beforeEach(() => {
|
||||
// UserFactory.create(1)
|
||||
namespaces = NamespaceFactory.create(1)
|
||||
lists = ListFactory.create(1)
|
||||
projects = ProjectFactory.create(1)
|
||||
buckets = BucketFactory.create(1, {
|
||||
list_id: lists[0].id,
|
||||
project_id: projects[0].id,
|
||||
})
|
||||
TaskFactory.truncate()
|
||||
UserListFactory.truncate()
|
||||
UserProjectFactory.truncate()
|
||||
})
|
||||
|
||||
it('Should be created new', () => {
|
||||
cy.visit('/lists/1/list')
|
||||
cy.visit('/projects/1/list')
|
||||
cy.get('.input[placeholder="Add a new task…"')
|
||||
.type('New Task')
|
||||
cy.get('.button')
|
||||
@ -74,11 +74,11 @@ describe('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)
|
||||
|
||||
cy.visit('/lists/1/list')
|
||||
cy.get('.list-is-empty-notice')
|
||||
cy.visit('/projects/1/list')
|
||||
cy.get('.project-is-empty-notice')
|
||||
.should('not.exist')
|
||||
cy.get('.input[placeholder="Add a new task…"')
|
||||
.type('New Task')
|
||||
@ -95,7 +95,7 @@ describe('Task', () => {
|
||||
it('Marks a task as done', () => {
|
||||
TaskFactory.create(1)
|
||||
|
||||
cy.visit('/lists/1/list')
|
||||
cy.visit('/projects/1/list')
|
||||
cy.get('.tasks .task .fancycheckbox label.check')
|
||||
.first()
|
||||
.click()
|
||||
@ -106,7 +106,7 @@ describe('Task', () => {
|
||||
it('Can add a task to favorites', () => {
|
||||
TaskFactory.create(1)
|
||||
|
||||
cy.visit('/lists/1/list')
|
||||
cy.visit('/projects/1/list')
|
||||
cy.get('.tasks .task .favorite')
|
||||
.first()
|
||||
.click()
|
||||
@ -134,7 +134,7 @@ describe('Task', () => {
|
||||
.should('contain', '#1')
|
||||
cy.get('.task-view h6.subtitle')
|
||||
.should('contain', namespaces[0].title)
|
||||
.should('contain', lists[0].title)
|
||||
.should('contain', projects[0].title)
|
||||
cy.get('.task-view .details.content.description')
|
||||
.should('contain', tasks[0].description)
|
||||
cy.get('.task-view .action-buttons p.created')
|
||||
@ -179,21 +179,21 @@ describe('Task', () => {
|
||||
.should('contain', 'Mark as undone')
|
||||
})
|
||||
|
||||
it('Shows a task identifier since the list has one', () => {
|
||||
const lists = ListFactory.create(1, {
|
||||
it('Shows a task identifier since the project has one', () => {
|
||||
const projects = ProjectFactory.create(1, {
|
||||
id: 1,
|
||||
identifier: 'TEST',
|
||||
})
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: lists[0].id,
|
||||
project_id: projects[0].id,
|
||||
index: 1,
|
||||
})
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].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', () => {
|
||||
@ -236,14 +236,14 @@ describe('Task', () => {
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
||||
it('Can move a task to another list', () => {
|
||||
const lists = ListFactory.create(2)
|
||||
it('Can move a task to another project', () => {
|
||||
const projects = ProjectFactory.create(2)
|
||||
BucketFactory.create(2, {
|
||||
list_id: '{increment}'
|
||||
project_id: '{increment}'
|
||||
})
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: lists[0].id,
|
||||
project_id: projects[0].id,
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
@ -251,7 +251,7 @@ describe('Task', () => {
|
||||
.contains('Move')
|
||||
.click()
|
||||
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
|
||||
// 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')
|
||||
@ -261,7 +261,7 @@ describe('Task', () => {
|
||||
|
||||
cy.get('.task-view h6.subtitle')
|
||||
.should('contain', namespaces[0].title)
|
||||
.should('contain', lists[1].title)
|
||||
.should('contain', projects[1].title)
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
@ -269,7 +269,7 @@ describe('Task', () => {
|
||||
it('Can delete a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
@ -286,17 +286,17 @@ describe('Task', () => {
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.url()
|
||||
.should('contain', `/lists/${tasks[0].list_id}/`)
|
||||
.should('contain', `/projects/${tasks[0].project_id}/`)
|
||||
})
|
||||
|
||||
it('Can add an assignee to a task', () => {
|
||||
const users = UserFactory.create(5)
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
UserListFactory.create(5, {
|
||||
list_id: 1,
|
||||
UserProjectFactory.create(5, {
|
||||
project_id: 1,
|
||||
user_id: '{increment}',
|
||||
})
|
||||
|
||||
@ -321,10 +321,10 @@ describe('Task', () => {
|
||||
const users = UserFactory.create(2)
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
UserListFactory.create(5, {
|
||||
list_id: 1,
|
||||
UserProjectFactory.create(5, {
|
||||
project_id: 1,
|
||||
user_id: '{increment}',
|
||||
})
|
||||
TaskAssigneeFactory.create(1, {
|
||||
@ -347,7 +347,7 @@ describe('Task', () => {
|
||||
it('Can add a new label to a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
LabelFactory.truncate()
|
||||
const newLabelText = 'some new label'
|
||||
@ -375,7 +375,7 @@ describe('Task', () => {
|
||||
it('Can add an existing label to a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
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', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: lists[0].id,
|
||||
project_id: projects[0].id,
|
||||
bucket_id: buckets[0].id,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
LabelTaskFactory.truncate()
|
||||
|
||||
cy.visit(`/lists/${lists[0].id}/kanban`)
|
||||
cy.visit(`/projects/${projects[0].id}/kanban`)
|
||||
|
||||
cy.get('.bucket .task')
|
||||
.contains(tasks[0].title)
|
||||
@ -412,7 +412,7 @@ describe('Task', () => {
|
||||
it('Can remove a label from a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
LabelTaskFactory.create(1, {
|
||||
@ -527,13 +527,13 @@ describe('Task', () => {
|
||||
TaskAttachmentFactory.truncate()
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: lists[0].id,
|
||||
project_id: projects[0].id,
|
||||
bucket_id: buckets[0].id,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
LabelTaskFactory.truncate()
|
||||
|
||||
cy.visit(`/lists/${lists[0].id}/kanban`)
|
||||
cy.visit(`/projects/${projects[0].id}/kanban`)
|
||||
|
||||
cy.get('.bucket .task')
|
||||
.contains(tasks[0].title)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
||||
import {createLists} from '../list/prepareLists'
|
||||
import {createProjects} from '../project/prepareProjects'
|
||||
|
||||
function logout() {
|
||||
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', () => {
|
||||
const lists = createLists()
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
it.skip('Should clear the project history after logging the user out', () => {
|
||||
const projects = createProjects()
|
||||
cy.visit(`/projects/${projects[0].id}`)
|
||||
.then(() => {
|
||||
expect(localStorage.getItem('listHistory')).to.not.eq(null)
|
||||
expect(localStorage.getItem('projectHistory')).to.not.eq(null)
|
||||
})
|
||||
|
||||
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()
|
||||
.should('contain', '/login')
|
||||
.then(() => {
|
||||
expect(localStorage.getItem('listHistory')).to.eq(null)
|
||||
expect(localStorage.getItem('projectHistory')).to.eq(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -10,7 +10,7 @@ export class BucketFactory extends Factory {
|
||||
return {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
created_by_id: 1,
|
||||
created: now.toISOString(),
|
||||
updated: now.toISOString(),
|
||||
|
@ -10,7 +10,7 @@ export class LinkShareFactory extends Factory {
|
||||
return {
|
||||
id: '{increment}',
|
||||
hash: faker.random.word(32),
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
right: 0,
|
||||
sharing_type: 0,
|
||||
shared_by_id: 1,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Factory} from '../support/factory'
|
||||
import {faker} from '@faker-js/faker'
|
||||
|
||||
export class ListFactory extends Factory {
|
||||
static table = 'lists'
|
||||
export class ProjectFactory extends Factory {
|
||||
static table = 'projects'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
@ -11,7 +11,7 @@ export class TaskFactory extends Factory {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(3),
|
||||
done: false,
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
created_by_id: 1,
|
||||
index: '{increment}',
|
||||
position: '{increment}',
|
||||
|
@ -1,14 +1,14 @@
|
||||
import {Factory} from '../support/factory'
|
||||
|
||||
export class UserListFactory extends Factory {
|
||||
static table = 'users_lists'
|
||||
export class UserProjectFactory extends Factory {
|
||||
static table = 'users_projects'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
project_id: 1,
|
||||
user_id: 1,
|
||||
right: 0,
|
||||
created: now.toISOString(),
|
@ -30,21 +30,21 @@ A basic service can look like this:
|
||||
|
||||
```javascript
|
||||
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() {
|
||||
super({
|
||||
getAll: '/lists',
|
||||
get: '/lists/{id}',
|
||||
create: '/namespaces/{namespaceID}/lists',
|
||||
update: '/lists/{id}',
|
||||
delete: '/lists/{id}',
|
||||
getAll: '/projects',
|
||||
get: '/projects/{id}',
|
||||
create: '/namespaces/{namespaceID}/projects',
|
||||
update: '/projects/{id}',
|
||||
delete: '/projects/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new ListModel(data)
|
||||
return new ProjectModel(data)
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -132,7 +132,7 @@ import AbstractModel from './abstractModel'
|
||||
import TaskModel from './task'
|
||||
import UserModel from './user'
|
||||
|
||||
export default class ListModel extends AbstractModel {
|
||||
export default class ProjectModel extends AbstractModel {
|
||||
|
||||
constructor(data) {
|
||||
// The constructor of AbstractModel handles all the default parsing.
|
||||
|
@ -1,67 +1,50 @@
|
||||
<template>
|
||||
<header
|
||||
:class="{'has-background': background, 'menu-active': menuActive}"
|
||||
aria-label="main navigation"
|
||||
class="navbar d-print-none"
|
||||
>
|
||||
<router-link :to="{name: 'home'}" class="logo-link">
|
||||
<Logo width="164" height="48"/>
|
||||
<header :class="{ 'has-background': background, 'menu-active': menuActive }" aria-label="main navigation"
|
||||
class="navbar d-print-none">
|
||||
<router-link :to="{ name: 'home' }" class="logo-link">
|
||||
<Logo width="164" height="48" />
|
||||
</router-link>
|
||||
|
||||
<MenuButton class="menu-button"/>
|
||||
<MenuButton class="menu-button" />
|
||||
|
||||
<div
|
||||
v-if="currentList.id"
|
||||
class="list-title-wrapper"
|
||||
>
|
||||
<h1 class="list-title">{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}</h1>
|
||||
|
||||
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="list-title-button">
|
||||
<icon icon="circle-info"/>
|
||||
<div v-if="currentProject.id" class="project-title-wrapper">
|
||||
<h1 class="project-title">{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
|
||||
</h1>
|
||||
|
||||
<BaseButton :to="{ name: 'project.info', params: { projectId: currentProject.id } }" class="project-title-button">
|
||||
<icon icon="circle-info" />
|
||||
</BaseButton>
|
||||
|
||||
<list-settings-dropdown
|
||||
v-if="canWriteCurrentList && currentList.id !== -1"
|
||||
class="list-title-dropdown"
|
||||
:list="currentList"
|
||||
>
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="list-title-button" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
<project-settings-dropdown v-if="canWriteCurrentProject && currentProject.id !== -1"
|
||||
class="project-title-dropdown" :project="currentProject">
|
||||
<template #trigger="{ toggleOpen }">
|
||||
<BaseButton class="project-title-button" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon" />
|
||||
</BaseButton>
|
||||
</template>
|
||||
</list-settings-dropdown>
|
||||
</project-settings-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<BaseButton
|
||||
@click="openQuickActions"
|
||||
class="trigger-button"
|
||||
v-shortcut="'Control+k'"
|
||||
:title="$t('keyboardShortcuts.quickSearch')"
|
||||
>
|
||||
<icon icon="search"/>
|
||||
<BaseButton @click="openQuickActions" class="trigger-button" v-shortcut="'Control+k'"
|
||||
:title="$t('keyboardShortcuts.quickSearch')">
|
||||
<icon icon="search" />
|
||||
</BaseButton>
|
||||
<Notifications />
|
||||
<dropdown>
|
||||
<template #trigger="{toggleOpen, open}">
|
||||
<BaseButton
|
||||
class="username-dropdown-trigger"
|
||||
@click="toggleOpen"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
|
||||
<template #trigger="{ toggleOpen, open }">
|
||||
<BaseButton class="username-dropdown-trigger" @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="icon is-small" :style="{
|
||||
transform: open ? 'rotate(180deg)' : 'rotate(0)',
|
||||
}">
|
||||
<icon icon="chevron-down"/>
|
||||
<icon icon="chevron-down" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<dropdown-item :to="{name: 'user.settings'}">
|
||||
<dropdown-item :to="{ name: 'user.settings' }">
|
||||
{{ $t('user.settings.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item v-if="imprintUrl" :href="imprintUrl">
|
||||
@ -73,7 +56,7 @@
|
||||
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
|
||||
{{ $t('keyboardShortcuts.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item :to="{name: 'about'}">
|
||||
<dropdown-item :to="{ name: 'about' }">
|
||||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item @click="authStore.logout()">
|
||||
@ -85,11 +68,11 @@
|
||||
</template>
|
||||
|
||||
<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 DropdownItem from '@/components/misc/dropdown-item.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 MenuButton from '@/components/home/MenuButton.vue'
|
||||
|
||||
import {getListTitle} from '@/helpers/getListTitle'
|
||||
import { getProjectTitle } from '@/helpers/getProjectTitle'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import { useBaseStore } from '@/stores/base'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
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 authStore = useAuthStore()
|
||||
@ -166,7 +149,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||
|
||||
.logo-link {
|
||||
display: none;
|
||||
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
@ -185,12 +168,12 @@ $user-dropdown-width-mobile: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-wrapper {
|
||||
.project-title-wrapper {
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
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
|
||||
min-width: 0;
|
||||
|
||||
@ -199,7 +182,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title {
|
||||
.project-title {
|
||||
font-size: 1rem;
|
||||
// We need the following for overflowing ellipsis to work
|
||||
text-overflow: ellipsis;
|
||||
@ -211,15 +194,15 @@ $user-dropdown-width-mobile: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-dropdown {
|
||||
.project-title-dropdown {
|
||||
align-self: stretch;
|
||||
|
||||
.list-title-button {
|
||||
.project-title-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-button {
|
||||
.project-title-button {
|
||||
align-self: stretch;
|
||||
min-width: var(--navbar-button-min-width);
|
||||
display: flex;
|
||||
@ -235,7 +218,7 @@ $user-dropdown-width-mobile: 5rem;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
> * {
|
||||
>* {
|
||||
min-width: var(--navbar-button-min-width);
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@
|
||||
<quick-actions/>
|
||||
|
||||
<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"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
@ -87,7 +87,7 @@ function showKeyboardShortcuts() {
|
||||
const route = useRoute()
|
||||
|
||||
// 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) => {
|
||||
if (
|
||||
routeName &&
|
||||
@ -106,7 +106,7 @@ watch(() => route.name as string, (routeName) => {
|
||||
routeName.startsWith('user.settings')
|
||||
)
|
||||
) {
|
||||
baseStore.handleSetCurrentList({list: null})
|
||||
baseStore.handleSetCurrentProject({project: null})
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -9,9 +9,9 @@
|
||||
<Logo class="logo" v-if="logoVisible"/>
|
||||
<h1
|
||||
:class="{'m-0': !logoVisible}"
|
||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
||||
:style="{ 'opacity': currentProject.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
|
||||
{{ currentProject.title === '' ? $t('misc.loading') : currentProject.title }}
|
||||
</h1>
|
||||
<div class="box has-text-left view">
|
||||
<router-view/>
|
||||
@ -31,7 +31,7 @@ import Logo from '@/components/home/Logo.vue'
|
||||
import PoweredByLink from './PoweredByLink.vue'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
const background = computed(() => baseStore.background)
|
||||
const logoVisible = computed(() => baseStore.logoVisible)
|
||||
</script>
|
||||
|
@ -52,37 +52,37 @@
|
||||
<template v-for="(n, nk) in namespaces" :key="n.id">
|
||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
||||
<BaseButton
|
||||
@click="toggleLists(n.id)"
|
||||
@click="toggleProjects(n.id)"
|
||||
class="menu-label"
|
||||
v-tooltip="namespaceTitles[nk]"
|
||||
>
|
||||
<ColorBubble
|
||||
v-if="n.hexColor !== ''"
|
||||
:color="n.hexColor"
|
||||
class="mr-1"
|
||||
v-if="n.hexColor !== ''"
|
||||
:color="n.hexColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
<span class="name">{{ namespaceTitles[nk] }}</span>
|
||||
<div
|
||||
class="icon menu-item-icon is-small toggle-lists-icon pl-2"
|
||||
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
|
||||
class="icon menu-item-icon is-small toggle-lists-icon pl-2"
|
||||
:class="{'active': typeof projectsVisible[n.id] !== 'undefined' ? projectsVisible[n.id] : true}"
|
||||
>
|
||||
<icon icon="chevron-down"/>
|
||||
</div>
|
||||
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
|
||||
({{ namespaceListsCount[nk] }})
|
||||
({{ namespaceProjectsCount[nk] }})
|
||||
</span>
|
||||
</BaseButton>
|
||||
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
|
||||
</div>
|
||||
<!--
|
||||
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
|
||||
triggered by the change needs to have access to the current namespace
|
||||
-->
|
||||
<draggable
|
||||
v-if="listsVisible[n.id] ?? true"
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<draggable
|
||||
v-if="projectsVisible[n.id] ?? true"
|
||||
v-bind="dragOptions"
|
||||
:modelValue="activeLists[nk]"
|
||||
@update:modelValue="(lists) => updateActiveLists(n, lists)"
|
||||
:modelValue="activeProjects[nk]"
|
||||
@update:modelValue="(projects) => updateActiveProjects(n, projects)"
|
||||
group="namespace-lists"
|
||||
@start="() => drag = true"
|
||||
@end="saveListPosition"
|
||||
@ -100,46 +100,46 @@
|
||||
{ 'dragging-disabled': n.id < 0 }
|
||||
]
|
||||
}"
|
||||
>
|
||||
<template #item="{element: l}">
|
||||
<li
|
||||
class="list-menu loader-container is-loading-small"
|
||||
:class="{'is-loading': listUpdating[l.id]}"
|
||||
>
|
||||
<template #item="{element: l}">
|
||||
<li
|
||||
class="list-menu loader-container is-loading-small"
|
||||
: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
|
||||
:to="{ name: 'list.index', params: { listId: l.id} }"
|
||||
class="list-menu-link"
|
||||
:class="{'router-link-exact-active': currentList.id === l.id}"
|
||||
>
|
||||
<span class="icon menu-item-icon handle">
|
||||
<icon icon="grip-lines"/>
|
||||
</span>
|
||||
<ColorBubble
|
||||
<span class="icon menu-item-icon handle">
|
||||
<icon icon="grip-lines"/>
|
||||
</span>
|
||||
<ColorBubble
|
||||
v-if="l.hexColor !== ''"
|
||||
:color="l.hexColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
<span class="list-menu-title">{{ getListTitle(l) }}</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="l.id > 0"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': l.isFavorite}"
|
||||
@click="listStore.toggleListFavorite(l)"
|
||||
>
|
||||
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
|
||||
</BaseButton>
|
||||
<list-settings-dropdown class="menu-list-dropdown" :list="l" v-if="l.id > 0">
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</list-settings-dropdown>
|
||||
<span class="list-setting-spacer" v-else></span>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
/>
|
||||
<span class="list-menu-title">{{ getProjectTitle(l) }}</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="l.id > 0"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': l.isFavorite}"
|
||||
@click="projectStore.toggleProjectFavorite(l)"
|
||||
>
|
||||
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
|
||||
</BaseButton>
|
||||
<ProjectSettingsDropdown class="menu-list-dropdown" :project="l" v-if="l.id > 0">
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</ProjectSettingsDropdown>
|
||||
<span class="list-setting-spacer" v-else></span>
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
</nav>
|
||||
<PoweredByLink/>
|
||||
@ -152,20 +152,20 @@ import draggable from 'zhyswan-vuedraggable'
|
||||
import type {SortableEvent} from 'sortablejs'
|
||||
|
||||
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 PoweredByLink from '@/components/home/PoweredByLink.vue'
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
|
||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||
import {getListTitle} from '@/helpers/getListTitle'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const drag = ref(false)
|
||||
@ -176,7 +176,7 @@ const dragOptions = {
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
const menuActive = computed(() => baseStore.menuActive)
|
||||
const loading = computed(() => namespaceStore.isLoading)
|
||||
|
||||
@ -184,9 +184,9 @@ const loading = computed(() => namespaceStore.isLoading)
|
||||
const namespaces = computed(() => {
|
||||
return namespaceStore.namespaces.filter(n => !n.isArchived)
|
||||
})
|
||||
const activeLists = computed(() => {
|
||||
return namespaces.value.map(({lists}) => {
|
||||
return lists?.filter(item => {
|
||||
const activeProjects = computed(() => {
|
||||
return namespaces.value.map(({projects}) => {
|
||||
return projects?.filter(item => {
|
||||
return typeof item !== 'undefined' && !item.isArchived
|
||||
})
|
||||
})
|
||||
@ -196,45 +196,45 @@ const namespaceTitles = computed(() => {
|
||||
return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
|
||||
})
|
||||
|
||||
const namespaceListsCount = computed(() => {
|
||||
return namespaces.value.map((_, index) => activeLists.value[index]?.length ?? 0)
|
||||
const namespaceProjectsCount = computed(() => {
|
||||
return namespaces.value.map((_, index) => activeProjects.value[index]?.length ?? 0)
|
||||
})
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
function toggleLists(namespaceId: INamespace['id']) {
|
||||
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
|
||||
function toggleProjects(namespaceId: INamespace['id']) {
|
||||
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
|
||||
onBeforeMount(async () => {
|
||||
const namespaces = await namespaceStore.loadNamespaces()
|
||||
namespaces.forEach(n => {
|
||||
if (typeof listsVisible.value[n.id] === 'undefined') {
|
||||
listsVisible.value[n.id] = true
|
||||
if (typeof projectsVisible.value[n.id] === 'undefined') {
|
||||
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
|
||||
// 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
|
||||
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
|
||||
// To work around this, we merge the active projects with the archived ones. Doing so breaks the order
|
||||
// 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.
|
||||
const lists = [
|
||||
...activeLists,
|
||||
...namespace.lists.filter(l => l.isArchived),
|
||||
const projects = [
|
||||
...activeProjects,
|
||||
...namespace.projects.filter(l => l.isArchived),
|
||||
]
|
||||
|
||||
namespaceStore.setNamespaceById({
|
||||
...namespace,
|
||||
lists,
|
||||
projects,
|
||||
})
|
||||
}
|
||||
|
||||
const listUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
|
||||
const projectUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
|
||||
|
||||
async function saveListPosition(e: SortableEvent) {
|
||||
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 newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
|
||||
|
||||
const listsActive = activeLists.value[newNamespaceIndex]
|
||||
// If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive
|
||||
// array instead of using the position. Because the index is wrong in that case, dragging the list will fail.
|
||||
const projectsActive = activeProjects.value[newNamespaceIndex]
|
||||
// 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 project will fail.
|
||||
// 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 listBefore = listsActive[newIndex - 1] ?? null
|
||||
const listAfter = listsActive[newIndex + 1] ?? null
|
||||
listUpdating.value[list.id] = true
|
||||
const project = projectsActive[newIndex]
|
||||
const projectBefore = projectsActive[newIndex - 1] ?? null
|
||||
const projectAfter = projectsActive[newIndex + 1] ?? null
|
||||
projectUpdating.value[project.id] = true
|
||||
|
||||
const position = calculateItemPosition(
|
||||
listBefore !== null ? listBefore.position : null,
|
||||
listAfter !== null ? listAfter.position : null,
|
||||
projectBefore !== null ? projectBefore.position : null,
|
||||
projectAfter !== null ? projectAfter.position : null,
|
||||
)
|
||||
|
||||
try {
|
||||
// create a copy of the list in order to not violate pinia manipulation
|
||||
await listStore.updateList({
|
||||
...list,
|
||||
// create a copy of the project in order to not violate pinia manipulation
|
||||
await projectStore.updateProject({
|
||||
...project,
|
||||
position,
|
||||
namespaceId,
|
||||
})
|
||||
} finally {
|
||||
listUpdating.value[list.id] = false
|
||||
projectUpdating.value[project.id] = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<multiselect
|
||||
v-model="selectedLists"
|
||||
:search-results="foundLists"
|
||||
:loading="listService.loading"
|
||||
v-model="selectedProjects"
|
||||
:search-results="foundProjects"
|
||||
:loading="projectService.loading"
|
||||
:multiple="true"
|
||||
:placeholder="$t('list.search')"
|
||||
:placeholder="$t('project.search')"
|
||||
label="title"
|
||||
@search="findLists"
|
||||
@search="findProjects"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -15,49 +15,49 @@ import {computed, ref, shallowReactive, watchEffect, type PropType} from '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'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<IList[]>,
|
||||
type: Array as PropType<IProject[]>,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: IList[]): void
|
||||
(e: 'update:modelValue', value: IProject[]): void
|
||||
}>()
|
||||
|
||||
const lists = ref<IList[]>([])
|
||||
const projects = ref<IProject[]>([])
|
||||
|
||||
watchEffect(() => {
|
||||
lists.value = props.modelValue
|
||||
projects.value = props.modelValue
|
||||
})
|
||||
|
||||
const selectedLists = computed({
|
||||
const selectedProjects = computed({
|
||||
get() {
|
||||
return lists.value
|
||||
return projects.value
|
||||
},
|
||||
set: (value) => {
|
||||
lists.value = value
|
||||
projects.value = value
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
||||
const listService = shallowReactive(new ListService())
|
||||
const foundLists = ref<IList[]>([])
|
||||
const projectService = shallowReactive(new ProjectService())
|
||||
const foundProjects = ref<IProject[]>([])
|
||||
|
||||
async function findLists(query: string) {
|
||||
async function findProjects(query: string) {
|
||||
if (query === '') {
|
||||
foundLists.value = []
|
||||
foundProjects.value = []
|
||||
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
|
||||
foundLists.value = response.filter(({id}) => !includesById(lists.value, id))
|
||||
foundProjects.value = response.filter(({id}) => !includesById(projects.value, id))
|
||||
}
|
||||
</script>
|
@ -286,11 +286,11 @@ function handleCheckboxClick(e: Event) {
|
||||
console.debug('no index found')
|
||||
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()
|
||||
renderPreview()
|
||||
}
|
||||
|
@ -61,8 +61,8 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'list.kanban.title',
|
||||
available: (route) => route.name === 'list.kanban',
|
||||
title: 'project.kanban.title',
|
||||
available: (route) => route.name === 'project.kanban',
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.task.done',
|
||||
@ -71,26 +71,26 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.title',
|
||||
available: (route) => (route.name as string)?.startsWith('list.'),
|
||||
title: 'keyboardShortcuts.project.title',
|
||||
available: (route) => (route.name as string)?.startsWith('project.'),
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToListView',
|
||||
title: 'keyboardShortcuts.project.switchToProjectView',
|
||||
keys: ['g', 'l'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToGanttView',
|
||||
title: 'keyboardShortcuts.project.switchToGanttView',
|
||||
keys: ['g', 'g'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToTableView',
|
||||
title: 'keyboardShortcuts.project.switchToTableView',
|
||||
keys: ['g', 't'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToKanbanView',
|
||||
title: 'keyboardShortcuts.project.switchToKanbanView',
|
||||
keys: ['g', 'k'],
|
||||
combination: 'then',
|
||||
},
|
||||
|
@ -73,14 +73,14 @@ const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
if (disabled.value) {
|
||||
if (props.entity === 'list' && subscriptionEntity.value === 'namespace') {
|
||||
return t('task.subscription.subscribedListThroughParentNamespace')
|
||||
if (props.entity === 'project' && subscriptionEntity.value === 'namespace') {
|
||||
return t('task.subscription.subscribedProjectThroughParentNamespace')
|
||||
}
|
||||
if (props.entity === 'task' && subscriptionEntity.value === 'namespace') {
|
||||
return t('task.subscription.subscribedTaskThroughParentNamespace')
|
||||
}
|
||||
if (props.entity === 'task' && subscriptionEntity.value === 'list') {
|
||||
return t('task.subscription.subscribedTaskThroughParentList')
|
||||
if (props.entity === 'task' && subscriptionEntity.value === 'project') {
|
||||
return t('task.subscription.subscribedTaskThroughParentProject')
|
||||
}
|
||||
|
||||
return ''
|
||||
@ -91,10 +91,10 @@ const tooltipText = computed(() => {
|
||||
return props.modelValue !== null ?
|
||||
t('task.subscription.subscribedNamespace') :
|
||||
t('task.subscription.notSubscribedNamespace')
|
||||
case 'list':
|
||||
case 'project':
|
||||
return props.modelValue !== null ?
|
||||
t('task.subscription.subscribedList') :
|
||||
t('task.subscription.notSubscribedList')
|
||||
t('task.subscription.subscribedProject') :
|
||||
t('task.subscription.notSubscribedProject')
|
||||
case 'task':
|
||||
return props.modelValue !== null ?
|
||||
t('task.subscription.subscribedTask') :
|
||||
@ -133,8 +133,8 @@ async function subscribe() {
|
||||
case 'namespace':
|
||||
message = t('task.subscription.subscribeSuccessNamespace')
|
||||
break
|
||||
case 'list':
|
||||
message = t('task.subscription.subscribeSuccessList')
|
||||
case 'project':
|
||||
message = t('task.subscription.subscribeSuccessProject')
|
||||
break
|
||||
case 'task':
|
||||
message = t('task.subscription.subscribeSuccessTask')
|
||||
@ -156,8 +156,8 @@ async function unsubscribe() {
|
||||
case 'namespace':
|
||||
message = t('task.subscription.unsubscribeSuccessNamespace')
|
||||
break
|
||||
case 'list':
|
||||
message = t('task.subscription.unsubscribeSuccessList')
|
||||
case 'project':
|
||||
message = t('task.subscription.unsubscribeSuccessProject')
|
||||
break
|
||||
case 'task':
|
||||
message = t('task.subscription.unsubscribeSuccessTask')
|
||||
|
@ -30,10 +30,10 @@
|
||||
{{ $t('menu.share') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.create', params: { namespaceId: namespace.id } }"
|
||||
:to="{ name: 'project.create', params: { namespaceId: namespace.id } }"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('menu.newList') }}
|
||||
{{ $t('menu.newProject') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
|
||||
|
@ -117,9 +117,9 @@ function to(n, index) {
|
||||
case names.TASK_DELETED:
|
||||
// Nothing
|
||||
break
|
||||
case names.LIST_CREATED:
|
||||
case names.PROJECT_CREATED:
|
||||
to.name = 'task.index'
|
||||
to.params.listId = n.notification.list.id
|
||||
to.params.projectId = n.notification.project.id
|
||||
break
|
||||
case names.TEAM_MEMBER_ADDED:
|
||||
to.name = 'teams.edit'
|
||||
|
@ -1,56 +1,56 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ 'is-loading': listService.loading, 'is-archived': currentList.isArchived}"
|
||||
:class="{ 'is-loading': projectService.loading, 'is-archived': currentProject.isArchived}"
|
||||
class="loader-container"
|
||||
>
|
||||
<div class="switch-view-container">
|
||||
<div class="switch-view">
|
||||
<BaseButton
|
||||
v-shortcut="'g l'"
|
||||
:title="$t('keyboardShortcuts.list.switchToListView')"
|
||||
:title="$t('keyboardShortcuts.project.switchToProjectView')"
|
||||
class="switch-view-button"
|
||||
:class="{'is-active': viewName === 'list'}"
|
||||
:to="{ name: 'list.list', params: { listId } }"
|
||||
:class="{'is-active': viewName === 'project'}"
|
||||
:to="{ name: 'project.list', params: { projectId } }"
|
||||
>
|
||||
{{ $t('list.list.title') }}
|
||||
{{ $t('project.list.title') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-shortcut="'g g'"
|
||||
:title="$t('keyboardShortcuts.list.switchToGanttView')"
|
||||
:title="$t('keyboardShortcuts.project.switchToGanttView')"
|
||||
class="switch-view-button"
|
||||
: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
|
||||
v-shortcut="'g t'"
|
||||
:title="$t('keyboardShortcuts.list.switchToTableView')"
|
||||
:title="$t('keyboardShortcuts.project.switchToTableView')"
|
||||
class="switch-view-button"
|
||||
: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
|
||||
v-shortcut="'g k'"
|
||||
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
|
||||
:title="$t('keyboardShortcuts.project.switchToKanbanView')"
|
||||
class="switch-view-button"
|
||||
: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>
|
||||
</div>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<CustomTransition name="fade">
|
||||
<Message variant="warning" v-if="currentList.isArchived" class="mb-4">
|
||||
{{ $t('list.archived') }}
|
||||
<transition name="fade">
|
||||
<Message variant="warning" v-if="currentProject.isArchived" class="mb-4">
|
||||
{{ $t('project.archived') }}
|
||||
</Message>
|
||||
</CustomTransition>
|
||||
</transition>
|
||||
|
||||
<slot v-if="loadedListId"/>
|
||||
<slot v-if="loadedProjectId"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -60,20 +60,19 @@ import {useRoute} from 'vue-router'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
import ListService from '@/services/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
import ProjectService from '@/services/project'
|
||||
|
||||
import {getListTitle} from '@/helpers/getListTitle'
|
||||
import {saveListToHistory} from '@/modules/listHistory'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import {saveProjectToHistory} from '@/modules/projectHistory'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const props = defineProps({
|
||||
listId: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
@ -86,64 +85,64 @@ const props = defineProps({
|
||||
const route = useRoute()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const listStore = useListStore()
|
||||
const listService = ref(new ListService())
|
||||
const loadedListId = ref(0)
|
||||
const projectStore = useProjectStore()
|
||||
const projectService = ref(new ProjectService())
|
||||
const loadedProjectId = ref(0)
|
||||
|
||||
const currentList = computed(() => {
|
||||
return typeof baseStore.currentList === 'undefined' ? {
|
||||
const currentProject = computed(() => {
|
||||
return typeof baseStore.currentProject === 'undefined' ? {
|
||||
id: 0,
|
||||
title: '',
|
||||
isArchived: false,
|
||||
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.
|
||||
// This resulted in loading and setting the list 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
|
||||
// list background and then navigating to home. It also highlighted the list in the menu and didn't allow changing any
|
||||
// This resulted in loading and setting the project multiple times, even when navigating away from it.
|
||||
// This caused wired bugs where the project background would be set on the home page but only right after setting a new
|
||||
// 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.
|
||||
watch(
|
||||
() => props.listId,
|
||||
// loadList
|
||||
async (listIdToLoad: number) => {
|
||||
const listData = {id: listIdToLoad}
|
||||
saveListToHistory(listData)
|
||||
() => props.projectId,
|
||||
// loadProject
|
||||
async (projectIdToLoad: number) => {
|
||||
const projectData = {id: projectIdToLoad}
|
||||
saveProjectToHistory(projectData)
|
||||
|
||||
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and
|
||||
// the currently loaded list has the right set.
|
||||
// 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 project has the right set.
|
||||
if (
|
||||
(
|
||||
listIdToLoad === loadedListId.value ||
|
||||
typeof listIdToLoad === 'undefined' ||
|
||||
listIdToLoad === currentList.value.id
|
||||
projectIdToLoad === loadedProjectId.value ||
|
||||
typeof projectIdToLoad === 'undefined' ||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
loadedListId.value = 0
|
||||
const listFromStore = listStore.getListById(listData.id)
|
||||
if (listFromStore !== null) {
|
||||
// Set the current project to the one we're about to load so that the title is already shown at the top
|
||||
loadedProjectId.value = 0
|
||||
const projectFromStore = projectStore.getProjectById(projectData.id)
|
||||
if (projectFromStore !== null) {
|
||||
baseStore.setBackground(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.
|
||||
const list = new ListModel(listData)
|
||||
// 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 project = new ProjectModel(projectData)
|
||||
try {
|
||||
const loadedList = await listService.value.get(list)
|
||||
baseStore.handleSetCurrentList({list: loadedList})
|
||||
const loadedProject = await projectService.value.get(project)
|
||||
baseStore.handleSetCurrentProject({project: loadedProject})
|
||||
} finally {
|
||||
loadedListId.value = props.listId
|
||||
loadedProjectId.value = props.projectId
|
||||
}
|
||||
},
|
||||
{immediate: true},
|
||||
|
@ -1,39 +1,39 @@
|
||||
<template>
|
||||
<div
|
||||
class="list-card"
|
||||
class="project-card"
|
||||
:class="{
|
||||
'has-light-text': background !== null,
|
||||
'has-background': blurHashUrl !== '' || background !== null
|
||||
}"
|
||||
: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,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="list-background background-fade-in"
|
||||
class="project-background background-fade-in"
|
||||
:class="{'is-visible': background}"
|
||||
: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
|
||||
class="list-button"
|
||||
:aria-label="list.title"
|
||||
:title="list.description"
|
||||
class="project-button"
|
||||
:aria-label="project.title"
|
||||
:title="project.description"
|
||||
:to="{
|
||||
name: 'list.index',
|
||||
params: { listId: list.id}
|
||||
name: 'project.index',
|
||||
params: { projectId: project.id}
|
||||
}"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="!list.isArchived"
|
||||
v-if="!project.isArchived"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': list.isFavorite}"
|
||||
@click.prevent.stop="listStore.toggleListFavorite(list)"
|
||||
:class="{'is-favorite': project.isFavorite}"
|
||||
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"
|
||||
>
|
||||
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" />
|
||||
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -41,30 +41,30 @@
|
||||
<script lang="ts" setup>
|
||||
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 {useListBackground} from './useListBackground'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectBackground} from './useProjectBackground'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Object as PropType<IList>,
|
||||
project: {
|
||||
type: Object as PropType<IProject>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const {background, blurHashUrl} = useListBackground(toRef(props, 'list'))
|
||||
const {background, blurHashUrl} = useProjectBackground(toRef(props, 'project'))
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-card {
|
||||
--list-card-padding: 1rem;
|
||||
.project-card {
|
||||
--project-card-padding: 1rem;
|
||||
background: var(--white);
|
||||
padding: var(--list-card-padding);
|
||||
padding: var(--project-card-padding);
|
||||
border-radius: $radius;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow $transition;
|
||||
@ -91,14 +91,14 @@ const listStore = useListStore()
|
||||
}
|
||||
|
||||
.has-background,
|
||||
.list-background {
|
||||
.project-background {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.list-background,
|
||||
.list-button {
|
||||
.project-background,
|
||||
.project-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@ -111,7 +111,7 @@ const listStore = useListStore()
|
||||
float: left;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
.project-title {
|
||||
align-self: flex-end;
|
||||
font-family: $vikunja-font;
|
||||
font-weight: 400;
|
||||
@ -120,7 +120,7 @@ const listStore = useListStore()
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
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;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
@ -130,11 +130,11 @@ const listStore = useListStore()
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.has-light-text .list-title {
|
||||
.has-light-text .project-title {
|
||||
color: var(--grey-100);
|
||||
}
|
||||
|
||||
.has-background .list-title {
|
||||
.has-background .project-title {
|
||||
text-shadow:
|
||||
0 0 10px var(--black),
|
||||
1px 1px 5px var(--grey-700),
|
||||
@ -144,8 +144,8 @@ const listStore = useListStore()
|
||||
|
||||
.favorite {
|
||||
position: absolute;
|
||||
top: var(--list-card-padding);
|
||||
right: var(--list-card-padding);
|
||||
top: var(--project-card-padding);
|
||||
right: var(--project-card-padding);
|
||||
transition: opacity $transition, color $transition;
|
||||
opacity: 1;
|
||||
|
||||
@ -165,7 +165,7 @@ const listStore = useListStore()
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.list-card:hover .favorite {
|
||||
.project-card:hover .favorite {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<ul class="list-grid">
|
||||
<ul class="project-grid">
|
||||
<li
|
||||
v-for="(item, index) in filteredLists"
|
||||
:key="`list_${item.id}_${index}`"
|
||||
class="list-grid-item"
|
||||
v-for="(item, index) in filteredProjects"
|
||||
:key="`project_${item.id}_${index}`"
|
||||
class="project-grid-item"
|
||||
>
|
||||
<ListCard :list="item" />
|
||||
<ProjectCard :project="item" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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({
|
||||
lists: {
|
||||
type: Array as PropType<IList[]>,
|
||||
projects: {
|
||||
type: Array as PropType<IProject[]>,
|
||||
default: () => [],
|
||||
},
|
||||
showArchived: {
|
||||
@ -31,46 +31,46 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const filteredLists = computed(() => {
|
||||
const filteredProjects = computed(() => {
|
||||
return props.showArchived
|
||||
? props.lists
|
||||
: props.lists.filter(l => !l.isArchived)
|
||||
? props.projects
|
||||
: props.projects.filter(l => !l.isArchived)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$list-height: 150px;
|
||||
$list-spacing: 1rem;
|
||||
$project-height: 150px;
|
||||
$project-spacing: 1rem;
|
||||
|
||||
.list-grid {
|
||||
.project-grid {
|
||||
margin: 0; // reset li
|
||||
list-style-type: none;
|
||||
project-style-type: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--list-columns), 1fr);
|
||||
grid-auto-rows: $list-height;
|
||||
gap: $list-spacing;
|
||||
grid-template-columns: repeat(var(--project-columns), 1fr);
|
||||
grid-auto-rows: $project-height;
|
||||
gap: $project-spacing;
|
||||
|
||||
@media screen and (min-width: $mobile) {
|
||||
--list-rows: 4;
|
||||
--list-columns: 1;
|
||||
--project-rows: 4;
|
||||
--project-columns: 1;
|
||||
}
|
||||
|
||||
@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) {
|
||||
--list-columns: 3;
|
||||
--list-rows: 3;
|
||||
--project-columns: 3;
|
||||
--project-rows: 3;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
--list-columns: 5;
|
||||
--list-rows: 2;
|
||||
--project-columns: 5;
|
||||
--project-rows: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.list-grid-item {
|
||||
.project-grid-item {
|
||||
display: grid;
|
||||
margin-top: 0; // remove padding coming form .content li + li
|
||||
}
|
||||
|
@ -32,7 +32,7 @@
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
|
@ -20,7 +20,7 @@
|
||||
{{ $t('filters.attributes.showDoneTasks') }}
|
||||
</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"
|
||||
@update:model-value="change()"
|
||||
>
|
||||
@ -154,14 +154,14 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label class="label">{{ $t('list.lists') }}</label>
|
||||
<label class="label">{{ $t('project.lists') }}</label>
|
||||
<div class="control">
|
||||
<SelectList
|
||||
v-model="entities.lists"
|
||||
@select="changeMultiselectFilter('lists', 'list_id')"
|
||||
@remove="changeMultiselectFilter('lists', 'list_id')"
|
||||
<SelectProject
|
||||
v-model="entities.projects"
|
||||
@select="changeMultiselectFilter('projects', 'project_id')"
|
||||
@remove="changeMultiselectFilter('projects', 'project_id')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -190,7 +190,7 @@ import {camelCase} from 'camel-case'
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
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 Fancycheckbox from '@/components/input/fancycheckbox.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 {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
@ -208,13 +208,13 @@ import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
|
||||
import UserService from '@/services/user'
|
||||
import ListService from '@/services/list'
|
||||
import ProjectService from '@/services/project'
|
||||
import NamespaceService from '@/services/namespace'
|
||||
|
||||
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskList.js
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskProject.js
|
||||
const DEFAULT_PARAMS = {
|
||||
sort_by: [],
|
||||
order_by: [],
|
||||
@ -239,7 +239,7 @@ const DEFAULT_FILTERS = {
|
||||
reminders: '',
|
||||
assignees: '',
|
||||
labels: '',
|
||||
list_id: '',
|
||||
project_id: '',
|
||||
namespace: '',
|
||||
} as const
|
||||
|
||||
@ -264,23 +264,23 @@ const filters = ref({...DEFAULT_FILTERS})
|
||||
|
||||
const services = {
|
||||
users: shallowReactive(new UserService()),
|
||||
lists: shallowReactive(new ListService()),
|
||||
projects: shallowReactive(new ProjectService()),
|
||||
namespace: shallowReactive(new NamespaceService()),
|
||||
}
|
||||
|
||||
interface Entities {
|
||||
users: IUser[]
|
||||
labels: ILabel[]
|
||||
lists: IList[]
|
||||
projects: IProject[]
|
||||
namespace: INamespace[]
|
||||
}
|
||||
|
||||
type EntityType = 'users' | 'labels' | 'lists' | 'namespace'
|
||||
type EntityType = 'users' | 'labels' | 'projects' | 'namespace'
|
||||
|
||||
const entities: Entities = reactive({
|
||||
users: [],
|
||||
labels: [],
|
||||
lists: [],
|
||||
projects: [],
|
||||
namespace: [],
|
||||
})
|
||||
|
||||
@ -327,7 +327,7 @@ function prepareFilters() {
|
||||
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
|
||||
prepareDate('reminders')
|
||||
prepareRelatedObjectFilter('users', 'assignees')
|
||||
prepareRelatedObjectFilter('lists', 'list_id')
|
||||
prepareRelatedObjectFilter('projects', 'project_id')
|
||||
prepareRelatedObjectFilter('namespace')
|
||||
|
||||
prepareSingleValue('labels')
|
@ -1,30 +1,30 @@
|
||||
import {ref, watch, type Ref} from 'vue'
|
||||
import ListService from '@/services/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import ProjectService from '@/services/project'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
||||
export function useListBackground(list: Ref<IList>) {
|
||||
export function useProjectBackground(project: Ref<IProject>) {
|
||||
const background = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
const blurHashUrl = ref('')
|
||||
|
||||
watch(
|
||||
() => [list.value.id, list.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']],
|
||||
async ([listId, blurHash], oldValue) => {
|
||||
() => [project.value.id, project.value.backgroundBlurHash] as [IProject['id'], IProject['backgroundBlurHash']],
|
||||
async ([projectId, blurHash], oldValue) => {
|
||||
if (
|
||||
list.value === null ||
|
||||
!list.value.backgroundInformation ||
|
||||
project.value === null ||
|
||||
!project.value.backgroundInformation ||
|
||||
backgroundLoading.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const [oldListId, oldBlurHash] = oldValue || []
|
||||
const [oldProjectId, oldBlurHash] = oldValue || []
|
||||
if (
|
||||
oldValue !== undefined &&
|
||||
listId === oldListId && blurHash === oldBlurHash
|
||||
oldValue !== undefined &&
|
||||
projectId === oldProjectId && blurHash === oldBlurHash
|
||||
) {
|
||||
// list hasn't changed
|
||||
// project hasn't changed
|
||||
return
|
||||
}
|
||||
|
||||
@ -35,8 +35,8 @@ export function useListBackground(list: Ref<IList>) {
|
||||
blurHashUrl.value = blurHash ? window.URL.createObjectURL(blurHash) : ''
|
||||
})
|
||||
|
||||
const listService = new ListService()
|
||||
const backgroundPromise = listService.background(list.value).then((result) => {
|
||||
const projectService = new ProjectService()
|
||||
const backgroundPromise = projectService.background(project.value).then((result) => {
|
||||
background.value = result
|
||||
})
|
||||
await Promise.all([blurHashPromise, backgroundPromise])
|
||||
@ -44,7 +44,7 @@ export function useListBackground(list: Ref<IList>) {
|
||||
backgroundLoading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
return {
|
||||
@ -52,4 +52,4 @@ export function useListBackground(list: Ref<IList>) {
|
||||
blurHashUrl,
|
||||
backgroundLoading,
|
||||
}
|
||||
}
|
||||
}
|
@ -8,24 +8,24 @@
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="isSavedFilter(list)">
|
||||
<template v-if="isSavedFilter(project)">
|
||||
<dropdown-item
|
||||
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
|
||||
:to="{ name: 'filter.settings.edit', params: { projectId: project.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
</dropdown-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="list.isArchived">
|
||||
<template v-else-if="project.isArchived">
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
@ -33,32 +33,32 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.edit', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.edit', params: { projectId: project.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="backgroundsEnabled"
|
||||
:to="{ name: 'list.settings.background', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.background', params: { projectId: project.id } }"
|
||||
icon="image"
|
||||
>
|
||||
{{ $t('menu.setBackground') }}
|
||||
</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"
|
||||
>
|
||||
{{ $t('menu.share') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.duplicate', params: { projectId: project.id } }"
|
||||
icon="paste"
|
||||
>
|
||||
{{ $t('menu.duplicate') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
@ -66,14 +66,14 @@
|
||||
<Subscription
|
||||
class="has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="list"
|
||||
:entity-id="list.id"
|
||||
:model-value="list.subscription"
|
||||
entity="project"
|
||||
:entity-id="project.id"
|
||||
:model-value="project.subscription"
|
||||
@update:model-value="setSubscriptionInStore"
|
||||
type="dropdown"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
|
||||
:to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
>
|
||||
@ -90,26 +90,26 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.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 {isSavedFilter} from '@/services/savedFilter'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Object as PropType<IList>,
|
||||
project: {
|
||||
type: Object as PropType<IProject>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const subscription = ref<ISubscription | null>(null)
|
||||
watchEffect(() => {
|
||||
subscription.value = props.list.subscription ?? null
|
||||
subscription.value = props.project.subscription ?? null
|
||||
})
|
||||
|
||||
const configStore = useConfigStore()
|
||||
@ -117,11 +117,11 @@ const backgroundsEnabled = computed(() => configStore.enabledBackgroundProviders
|
||||
|
||||
function setSubscriptionInStore(sub: ISubscription) {
|
||||
subscription.value = sub
|
||||
const updatedList = {
|
||||
...props.list,
|
||||
const updatedProject = {
|
||||
...props.project,
|
||||
subscription: sub,
|
||||
}
|
||||
listStore.setList(updatedList)
|
||||
namespaceStore.setListInNamespaceById(updatedList)
|
||||
projectStore.setProject(updatedProject)
|
||||
namespaceStore.setProjectInNamespaceById(updatedProject)
|
||||
}
|
||||
</script>
|
@ -63,18 +63,18 @@ import TeamService from '@/services/team'
|
||||
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
import TeamModel from '@/models/team'
|
||||
import ListModel from '@/models/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
import {getHistory} from '@/modules/listHistory'
|
||||
import {getHistory} from '@/modules/projectHistory'
|
||||
import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText'
|
||||
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
import {success} from '@/message'
|
||||
@ -82,13 +82,13 @@ import {success} from '@/message'
|
||||
import type {ITeam} from '@/modelTypes/ITeam'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const router = useRouter()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const labelStore = useLabelStore()
|
||||
const taskStore = useTaskStore()
|
||||
@ -98,13 +98,13 @@ type DoAction<Type = any> = { type: ACTION_TYPE } & Type
|
||||
enum ACTION_TYPE {
|
||||
CMD = 'cmd',
|
||||
TASK = 'task',
|
||||
LIST = 'list',
|
||||
PROJECT = 'project',
|
||||
TEAM = 'team',
|
||||
}
|
||||
|
||||
enum COMMAND_TYPE {
|
||||
NEW_TASK = 'newTask',
|
||||
NEW_LIST = 'newList',
|
||||
NEW_PROJECT = 'newProject',
|
||||
NEW_NAMESPACE = 'newNamespace',
|
||||
NEW_TEAM = 'newTeam',
|
||||
}
|
||||
@ -112,7 +112,7 @@ enum COMMAND_TYPE {
|
||||
enum SEARCH_MODE {
|
||||
ALL = 'all',
|
||||
TASKS = 'tasks',
|
||||
LISTS = 'lists',
|
||||
PROJECTS = 'projects',
|
||||
TEAMS = 'teams',
|
||||
}
|
||||
|
||||
@ -137,26 +137,26 @@ function closeQuickActions() {
|
||||
baseStore.setQuickActionsActive(false)
|
||||
}
|
||||
|
||||
const foundLists = computed(() => {
|
||||
const { list } = parsedQuery.value
|
||||
const foundProjects = computed(() => {
|
||||
const { project } = parsedQuery.value
|
||||
if (
|
||||
searchMode.value === SEARCH_MODE.ALL ||
|
||||
searchMode.value === SEARCH_MODE.LISTS ||
|
||||
list === null
|
||||
searchMode.value === SEARCH_MODE.PROJECTS ||
|
||||
project === null
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ncache: { [id: ListModel['id']]: INamespace } = {}
|
||||
const ncache: { [id: ProjectModel['id']]: INamespace } = {}
|
||||
const history = getHistory()
|
||||
const allLists = [
|
||||
const allProjects = [
|
||||
...new Set([
|
||||
...history.map((l) => listStore.getListById(l.id)),
|
||||
...listStore.searchList(list),
|
||||
...history.map((l) => projectStore.getProjectById(l.id)),
|
||||
...projectStore.searchProject(project),
|
||||
]),
|
||||
]
|
||||
|
||||
return allLists.filter((l) => {
|
||||
return allProjects.filter((l) => {
|
||||
if (typeof l === 'undefined' || l === null) {
|
||||
return false
|
||||
}
|
||||
@ -191,9 +191,9 @@ const results = computed<Result[]>(() => {
|
||||
items: foundTasks.value,
|
||||
},
|
||||
{
|
||||
type: ACTION_TYPE.LIST,
|
||||
title: t('quickActions.lists'),
|
||||
items: foundLists.value,
|
||||
type: ACTION_TYPE.PROJECT,
|
||||
title: t('quickActions.projects'),
|
||||
items: foundProjects.value,
|
||||
},
|
||||
{
|
||||
type: ACTION_TYPE.TEAM,
|
||||
@ -206,7 +206,7 @@ const results = computed<Result[]>(() => {
|
||||
const loading = computed(() =>
|
||||
taskService.loading ||
|
||||
namespaceStore.isLoading ||
|
||||
listStore.isLoading ||
|
||||
projectStore.isLoading ||
|
||||
teamService.loading,
|
||||
)
|
||||
|
||||
@ -224,11 +224,11 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
|
||||
placeholder: t('quickActions.newTask'),
|
||||
action: newTask,
|
||||
},
|
||||
newList: {
|
||||
type: COMMAND_TYPE.NEW_LIST,
|
||||
title: t('quickActions.cmds.newList'),
|
||||
placeholder: t('quickActions.newList'),
|
||||
action: newList,
|
||||
newProject: {
|
||||
type: COMMAND_TYPE.NEW_PROJECT,
|
||||
title: t('quickActions.cmds.newProject'),
|
||||
placeholder: t('quickActions.newProject'),
|
||||
action: newProject,
|
||||
},
|
||||
newNamespace: {
|
||||
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 currentList = computed(() => Object.keys(baseStore.currentList).length === 0
|
||||
const currentProject = computed(() => Object.keys(baseStore.currentProject).length === 0
|
||||
? null
|
||||
: baseStore.currentList,
|
||||
: baseStore.currentProject,
|
||||
)
|
||||
|
||||
const hintText = computed(() => {
|
||||
let namespace
|
||||
if (selectedCmd.value !== null && currentList.value !== null) {
|
||||
if (selectedCmd.value !== null && currentProject.value !== null) {
|
||||
switch (selectedCmd.value.type) {
|
||||
case COMMAND_TYPE.NEW_TASK:
|
||||
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(
|
||||
currentList.value.namespaceId,
|
||||
currentProject.value.namespaceId,
|
||||
)
|
||||
return t('quickActions.createList', {
|
||||
return t('quickActions.createProject', {
|
||||
title: namespace?.title,
|
||||
})
|
||||
}
|
||||
@ -275,8 +275,8 @@ const hintText = computed(() => {
|
||||
|
||||
const availableCmds = computed(() => {
|
||||
const cmds = []
|
||||
if (currentList.value !== null) {
|
||||
cmds.push(commands.value.newTask, commands.value.newList)
|
||||
if (currentProject.value !== null) {
|
||||
cmds.push(commands.value.newTask, commands.value.newProject)
|
||||
}
|
||||
cmds.push(commands.value.newNamespace, commands.value.newTeam)
|
||||
return cmds
|
||||
@ -288,21 +288,21 @@ const searchMode = computed(() => {
|
||||
if (query.value === '') {
|
||||
return SEARCH_MODE.ALL
|
||||
}
|
||||
const { text, list, labels, assignees } = parsedQuery.value
|
||||
const { text, project, labels, assignees } = parsedQuery.value
|
||||
if (assignees.length === 0 && text !== '') {
|
||||
return SEARCH_MODE.TASKS
|
||||
}
|
||||
if (
|
||||
assignees.length === 0 &&
|
||||
list !== null &&
|
||||
project !== null &&
|
||||
text === '' &&
|
||||
labels.length === 0
|
||||
) {
|
||||
return SEARCH_MODE.LISTS
|
||||
return SEARCH_MODE.PROJECTS
|
||||
}
|
||||
if (
|
||||
assignees.length > 0 &&
|
||||
list === null &&
|
||||
project === null &&
|
||||
text === '' &&
|
||||
labels.length === 0
|
||||
) {
|
||||
@ -356,7 +356,7 @@ function searchTasks() {
|
||||
taskSearchTimeout.value = null
|
||||
}
|
||||
|
||||
const { text, list: listName, labels } = parsedQuery.value
|
||||
const { text, project: projectName, labels } = parsedQuery.value
|
||||
|
||||
const filters: Filter[] = []
|
||||
|
||||
@ -373,10 +373,10 @@ function searchTasks() {
|
||||
})
|
||||
}
|
||||
|
||||
if (listName !== null) {
|
||||
const list = listStore.findListByExactname(listName)
|
||||
if (list !== null) {
|
||||
addFilter('listId', list.id, 'equals')
|
||||
if (projectName !== null) {
|
||||
const project = projectStore.findProjectByExactname(projectName)
|
||||
if (project !== null) {
|
||||
addFilter('projectId', project.id, 'equals')
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,9 +396,9 @@ function searchTasks() {
|
||||
const r = await taskService.getAll({}, params) as DoAction<ITask>[]
|
||||
foundTasks.value = r.map((t) => {
|
||||
t.type = ACTION_TYPE.TASK
|
||||
const list = listStore.getListById(t.listId)
|
||||
if (list !== null) {
|
||||
t.title = `${t.title} (${list.title})`
|
||||
const project = projectStore.getProjectById(t.projectId)
|
||||
if (project !== null) {
|
||||
t.title = `${t.title} (${project.title})`
|
||||
}
|
||||
return t
|
||||
})
|
||||
@ -444,11 +444,11 @@ const searchInput = ref<HTMLElement | null>(null)
|
||||
|
||||
async function doAction(type: ACTION_TYPE, item: DoAction) {
|
||||
switch (type) {
|
||||
case ACTION_TYPE.LIST:
|
||||
case ACTION_TYPE.PROJECT:
|
||||
closeQuickActions()
|
||||
await router.push({
|
||||
name: 'list.index',
|
||||
params: { listId: (item as DoAction<IList>).id },
|
||||
name: 'project.index',
|
||||
params: { projectId: (item as DoAction<IProject>).id },
|
||||
})
|
||||
break
|
||||
case ACTION_TYPE.TASK:
|
||||
@ -489,29 +489,29 @@ async function doCmd() {
|
||||
}
|
||||
|
||||
async function newTask() {
|
||||
if (currentList.value === null) {
|
||||
if (currentProject.value === null) {
|
||||
return
|
||||
}
|
||||
const task = await taskStore.createNewTask({
|
||||
title: query.value,
|
||||
listId: currentList.value.id,
|
||||
projectId: currentProject.value.id,
|
||||
})
|
||||
success({ message: t('task.createSuccess') })
|
||||
await router.push({ name: 'task.detail', params: { id: task.id } })
|
||||
}
|
||||
|
||||
async function newList() {
|
||||
if (currentList.value === null) {
|
||||
async function newProject() {
|
||||
if (currentProject.value === null) {
|
||||
return
|
||||
}
|
||||
const newList = await listStore.createList(new ListModel({
|
||||
const newProject = await projectStore.createProject(new ProjectModel({
|
||||
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({
|
||||
name: 'list.index',
|
||||
params: { listId: newList.id },
|
||||
name: 'project.index',
|
||||
params: { projectId: newProject.id },
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,39 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ $t('list.share.links.title') }}
|
||||
{{ $t('project.share.links.title') }}
|
||||
<span
|
||||
class="is-size-7 has-text-grey is-italic ml-3"
|
||||
v-tooltip="$t('list.share.links.explanation')">
|
||||
{{ $t('list.share.links.what') }}
|
||||
v-tooltip="$t('project.share.links.explanation')">
|
||||
{{ $t('project.share.links.what') }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="sharables-list">
|
||||
<div class="sharables-project">
|
||||
<x-button
|
||||
v-if="!(linkShares.length === 0 || showNewForm)"
|
||||
@click="showNewForm = true"
|
||||
icon="plus"
|
||||
class="mb-4">
|
||||
{{ $t('list.share.links.create') }}
|
||||
{{ $t('project.share.links.create') }}
|
||||
</x-button>
|
||||
|
||||
<div class="p-4" v-if="linkShares.length === 0 || showNewForm">
|
||||
<div class="field">
|
||||
<label class="label" for="linkShareRight">
|
||||
{{ $t('list.share.right.title') }}
|
||||
{{ $t('project.share.right.title') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select v-model="selectedRight" id="linkShareRight">
|
||||
<option :value="RIGHTS.READ">
|
||||
{{ $t('list.share.right.read') }}
|
||||
{{ $t('project.share.right.read') }}
|
||||
</option>
|
||||
<option :value="RIGHTS.READ_WRITE">
|
||||
{{ $t('list.share.right.readWrite') }}
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</option>
|
||||
<option :value="RIGHTS.ADMIN">
|
||||
{{ $t('list.share.right.admin') }}
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -41,21 +41,21 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="linkShareName">
|
||||
{{ $t('list.share.links.name') }}
|
||||
{{ $t('project.share.links.name') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="linkShareName"
|
||||
class="input"
|
||||
:placeholder="$t('list.share.links.namePlaceholder')"
|
||||
v-tooltip="$t('list.share.links.nameExplanation')"
|
||||
:placeholder="$t('project.share.links.namePlaceholder')"
|
||||
v-tooltip="$t('project.share.links.nameExplanation')"
|
||||
v-model="name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="linkSharePassword">
|
||||
{{ $t('list.share.links.password') }}
|
||||
{{ $t('project.share.links.password') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
@ -63,25 +63,25 @@
|
||||
type="password"
|
||||
class="input"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
v-tooltip="$t('list.share.links.passwordExplanation')"
|
||||
v-tooltip="$t('project.share.links.passwordExplanation')"
|
||||
v-model="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<x-button @click="add(listId)" icon="plus">
|
||||
{{ $t('list.share.share') }}
|
||||
<x-button @click="add(projectId)" icon="plus">
|
||||
{{ $t('project.share.share') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ $t('list.share.links.view') }}</th>
|
||||
<th>{{ $t('list.share.attributes.delete') }}</th>
|
||||
<th>{{ $t('project.share.links.view') }}</th>
|
||||
<th>{{ $t('project.share.attributes.delete') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -92,7 +92,7 @@
|
||||
</p>
|
||||
|
||||
<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>
|
||||
</i18n-t>
|
||||
</p>
|
||||
@ -102,19 +102,19 @@
|
||||
<span class="icon is-small">
|
||||
<icon icon="lock"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.admin') }}
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</template>
|
||||
<template v-else-if="s.right === RIGHTS.READ_WRITE">
|
||||
<span class="icon is-small">
|
||||
<icon icon="pen"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.readWrite') }}
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="icon is-small">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.read') }}
|
||||
{{ $t('project.share.right.read') }}
|
||||
</template>
|
||||
</p>
|
||||
|
||||
@ -172,14 +172,14 @@
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@submit="remove(listId)"
|
||||
@submit="remove(projectId)"
|
||||
>
|
||||
<template #header>
|
||||
<span>{{ $t('list.share.links.remove') }}</span>
|
||||
<span>{{ $t('project.share.links.remove') }}</span>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('list.share.links.removeText') }}</p>
|
||||
<p>{{ $t('project.share.links.removeText') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
@ -193,19 +193,19 @@ import {RIGHTS} from '@/constants/rights'
|
||||
import LinkShareModel from '@/models/linkShare'
|
||||
|
||||
import type {ILinkShare} from '@/modelTypes/ILinkShare'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import LinkShareService from '@/services/linkShare'
|
||||
|
||||
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
||||
import {success} from '@/message'
|
||||
import {getDisplayName} from '@/models/user'
|
||||
import type {ListView} from '@/types/ListView'
|
||||
import {LIST_VIEWS} from '@/types/ListView'
|
||||
import type {ProjectView} from '@/types/ProjectView'
|
||||
import {PROJECT_VIEWS} from '@/types/ProjectView'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
const props = defineProps({
|
||||
listId: {
|
||||
projectId: {
|
||||
default: 0,
|
||||
required: true,
|
||||
},
|
||||
@ -222,20 +222,20 @@ const showDeleteModal = ref(false)
|
||||
const linkIdToDelete = ref(0)
|
||||
const showNewForm = ref(false)
|
||||
|
||||
type SelectedViewMapper = Record<IList['id'], ListView>
|
||||
type SelectedViewMapper = Record<IProject['id'], ProjectView>
|
||||
|
||||
const selectedView = ref<SelectedViewMapper>({})
|
||||
|
||||
const availableViews = computed<Record<ListView, string>>(() => ({
|
||||
list: t('list.list.title'),
|
||||
gantt: t('list.gantt.title'),
|
||||
table: t('list.table.title'),
|
||||
kanban: t('list.kanban.title'),
|
||||
const availableViews = computed<Record<ProjectView, string>>(() => ({
|
||||
list: t('project.list.title'),
|
||||
gantt: t('project.gantt.title'),
|
||||
table: t('project.table.title'),
|
||||
kanban: t('project.kanban.title'),
|
||||
}))
|
||||
|
||||
const copy = useCopyToClipboard()
|
||||
watch(
|
||||
() => props.listId,
|
||||
() => props.projectId,
|
||||
load,
|
||||
{immediate: true},
|
||||
)
|
||||
@ -243,23 +243,23 @@ watch(
|
||||
const configStore = useConfigStore()
|
||||
const frontendUrl = computed(() => configStore.frontendUrl)
|
||||
|
||||
async function load(listId: IList['id']) {
|
||||
// If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here
|
||||
if (listId === 0) {
|
||||
async function load(projectId: IProject['id']) {
|
||||
// If projectId == 0 the project on the calling component wasn't already loaded, so we just bail out here
|
||||
if (projectId === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const links = await linkShareService.getAll({listId})
|
||||
const links = await linkShareService.getAll({projectId})
|
||||
links.forEach((l: ILinkShare) => {
|
||||
selectedView.value[l.id] = 'list'
|
||||
selectedView.value[l.id] = 'project'
|
||||
})
|
||||
linkShares.value = links
|
||||
}
|
||||
|
||||
async function add(listId: IList['id']) {
|
||||
async function add(projectId: IProject['id']) {
|
||||
const newLinkShare = new LinkShareModel({
|
||||
right: selectedRight.value,
|
||||
listId,
|
||||
projectId,
|
||||
name: name.value,
|
||||
password: password.value,
|
||||
})
|
||||
@ -268,31 +268,31 @@ async function add(listId: IList['id']) {
|
||||
name.value = ''
|
||||
password.value = ''
|
||||
showNewForm.value = false
|
||||
success({message: t('list.share.links.createSuccess')})
|
||||
await load(listId)
|
||||
success({message: t('project.share.links.createSuccess')})
|
||||
await load(projectId)
|
||||
}
|
||||
|
||||
async function remove(listId: IList['id']) {
|
||||
async function remove(projectId: IProject['id']) {
|
||||
try {
|
||||
await linkShareService.delete(new LinkShareModel({
|
||||
id: linkIdToDelete.value,
|
||||
listId,
|
||||
projectId,
|
||||
}))
|
||||
success({message: t('list.share.links.deleteSuccess')})
|
||||
await load(listId)
|
||||
success({message: t('project.share.links.deleteSuccess')})
|
||||
await load(projectId)
|
||||
} finally {
|
||||
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
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// FIXME: I think this is not needed
|
||||
.sharables-list:not(.card-content) {
|
||||
.sharables-project:not(.card-content) {
|
||||
overflow-y: auto
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ $t('list.share.userTeam.shared', {type: shareTypeNames}) }}
|
||||
{{ $t('project.share.userTeam.shared', {type: shareTypeNames}) }}
|
||||
</p>
|
||||
<div v-if="userIsAdmin">
|
||||
<div class="field has-addons">
|
||||
@ -19,7 +19,7 @@
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<x-button @click="add()">{{ $t('list.share.share') }}</x-button>
|
||||
<x-button @click="add()">{{ $t('project.share.share') }}</x-button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -31,7 +31,7 @@
|
||||
<td>{{ getDisplayName(s) }}</td>
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
</template>
|
||||
@ -52,19 +52,19 @@
|
||||
<span class="icon is-small">
|
||||
<icon icon="lock"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.admin') }}
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</template>
|
||||
<template v-else-if="s.right === RIGHTS.READ_WRITE">
|
||||
<span class="icon is-small">
|
||||
<icon icon="pen"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.readWrite') }}
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="icon is-small">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
{{ $t('list.share.right.read') }}
|
||||
{{ $t('project.share.right.read') }}
|
||||
</template>
|
||||
</td>
|
||||
<td class="actions" v-if="userIsAdmin">
|
||||
@ -78,19 +78,19 @@
|
||||
:selected="s.right === RIGHTS.READ"
|
||||
:value="RIGHTS.READ"
|
||||
>
|
||||
{{ $t('list.share.right.read') }}
|
||||
{{ $t('project.share.right.read') }}
|
||||
</option>
|
||||
<option
|
||||
:selected="s.right === RIGHTS.READ_WRITE"
|
||||
:value="RIGHTS.READ_WRITE"
|
||||
>
|
||||
{{ $t('list.share.right.readWrite') }}
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</option>
|
||||
<option
|
||||
:selected="s.right === RIGHTS.ADMIN"
|
||||
:value="RIGHTS.ADMIN"
|
||||
>
|
||||
{{ $t('list.share.right.admin') }}
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -110,7 +110,7 @@
|
||||
</table>
|
||||
|
||||
<nothing v-else>
|
||||
{{ $t('list.share.userTeam.notShared', {type: shareTypeNames}) }}
|
||||
{{ $t('project.share.userTeam.notShared', {type: shareTypeNames}) }}
|
||||
</nothing>
|
||||
|
||||
<modal
|
||||
@ -120,11 +120,11 @@
|
||||
>
|
||||
<template #header>
|
||||
<span>{{
|
||||
$t('list.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
|
||||
$t('project.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
|
||||
}}</span>
|
||||
</template>
|
||||
<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>
|
||||
</modal>
|
||||
</div>
|
||||
@ -143,9 +143,9 @@ import UserNamespaceService from '@/services/userNamespace'
|
||||
import UserNamespaceModel from '@/models/userNamespace'
|
||||
import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
|
||||
|
||||
import UserListService from '@/services/userList'
|
||||
import UserListModel from '@/models/userList'
|
||||
import type {IUserList} from '@/modelTypes/IUserList'
|
||||
import UserProjectService from '@/services/userProject'
|
||||
import UserProjectModel from '@/models/userProject'
|
||||
import type {IUserProject} from '@/modelTypes/IUserProject'
|
||||
|
||||
import UserService from '@/services/user'
|
||||
import UserModel, { getDisplayName } from '@/models/user'
|
||||
@ -155,9 +155,9 @@ import TeamNamespaceService from '@/services/teamNamespace'
|
||||
import TeamNamespaceModel from '@/models/teamNamespace'
|
||||
import type { ITeamNamespace } from '@/modelTypes/ITeamNamespace'
|
||||
|
||||
import TeamListService from '@/services/teamList'
|
||||
import TeamListModel from '@/models/teamList'
|
||||
import type { ITeamList } from '@/modelTypes/ITeamList'
|
||||
import TeamProjectService from '@/services/teamProject'
|
||||
import TeamProjectModel from '@/models/teamProject'
|
||||
import type { ITeamProject } from '@/modelTypes/ITeamProject'
|
||||
|
||||
import TeamService from '@/services/team'
|
||||
import TeamModel from '@/models/team'
|
||||
@ -172,7 +172,7 @@ import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<'list' | 'namespace'>,
|
||||
type: String as PropType<'project' | 'namespace'>,
|
||||
default: '',
|
||||
},
|
||||
shareType: {
|
||||
@ -191,9 +191,9 @@ const props = defineProps({
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
// This user service is either a userNamespaceService or a userListService, depending on the type we are using
|
||||
let stuffService: UserNamespaceService | UserListService | TeamListService | TeamNamespaceService
|
||||
let stuffModel: IUserNamespace | IUserList | ITeamList | ITeamNamespace
|
||||
// This user service is either a userNamespaceService or a userProjectService, depending on the type we are using
|
||||
let stuffService: UserNamespaceService | UserProjectService | TeamProjectService | TeamNamespaceService
|
||||
let stuffModel: IUserNamespace | IUserProject | ITeamProject | ITeamNamespace
|
||||
let searchService: UserService | TeamService
|
||||
let sharable: Ref<IUser | ITeam>
|
||||
|
||||
@ -201,7 +201,7 @@ const searchLabel = 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 showDeleteModal = ref(false)
|
||||
|
||||
@ -212,11 +212,11 @@ const userInfo = computed(() => authStore.info)
|
||||
function createShareTypeNameComputed(count: number) {
|
||||
return computed(() => {
|
||||
if (props.shareType === 'user') {
|
||||
return t('list.share.userTeam.typeUser', count)
|
||||
return t('project.share.userTeam.typeUser', count)
|
||||
}
|
||||
|
||||
if (props.shareType === 'team') {
|
||||
return t('list.share.userTeam.typeTeam', count)
|
||||
return t('project.share.userTeam.typeTeam', count)
|
||||
}
|
||||
|
||||
return ''
|
||||
@ -227,8 +227,8 @@ const shareTypeNames = createShareTypeNameComputed(2)
|
||||
const shareTypeName = createShareTypeNameComputed(1)
|
||||
|
||||
const sharableName = computed(() => {
|
||||
if (props.type === 'list') {
|
||||
return t('list.list.title')
|
||||
if (props.type === 'project') {
|
||||
return t('project.list.title')
|
||||
}
|
||||
|
||||
if (props.shareType === 'namespace') {
|
||||
@ -244,9 +244,9 @@ if (props.shareType === 'user') {
|
||||
sharable = ref(new UserModel())
|
||||
searchLabel.value = 'username'
|
||||
|
||||
if (props.type === 'list') {
|
||||
stuffService = shallowReactive(new UserListService())
|
||||
stuffModel = reactive(new UserListModel({listId: props.id}))
|
||||
if (props.type === 'project') {
|
||||
stuffService = shallowReactive(new UserProjectService())
|
||||
stuffModel = reactive(new UserProjectModel({projectId: props.id}))
|
||||
} else if (props.type === 'namespace') {
|
||||
stuffService = shallowReactive(new UserNamespaceService())
|
||||
stuffModel = reactive(new UserNamespaceModel({
|
||||
@ -261,9 +261,9 @@ if (props.shareType === 'user') {
|
||||
sharable = ref(new TeamModel())
|
||||
searchLabel.value = 'name'
|
||||
|
||||
if (props.type === 'list') {
|
||||
stuffService = shallowReactive(new TeamListService())
|
||||
stuffModel = reactive(new TeamListModel({listId: props.id}))
|
||||
if (props.type === 'project') {
|
||||
stuffService = shallowReactive(new TeamProjectService())
|
||||
stuffModel = reactive(new TeamProjectModel({projectId: props.id}))
|
||||
} else if (props.type === 'namespace') {
|
||||
stuffService = shallowReactive(new TeamNamespaceService())
|
||||
stuffModel = reactive(new TeamNamespaceModel({
|
||||
@ -303,7 +303,7 @@ async function deleteSharable() {
|
||||
}
|
||||
}
|
||||
success({
|
||||
message: t('list.share.userTeam.removeSuccess', {
|
||||
message: t('project.share.userTeam.removeSuccess', {
|
||||
type: shareTypeName.value,
|
||||
sharable: sharableName.value,
|
||||
}),
|
||||
@ -326,7 +326,7 @@ async function add(admin) {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@ -358,7 +358,7 @@ async function toggleType(sharable) {
|
||||
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([])
|
||||
|
@ -50,7 +50,7 @@ import {parseKebabDate} from '@/helpers/time/parseKebabDate'
|
||||
|
||||
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
|
||||
import type {DateISO} from '@/types/DateISO'
|
||||
import type {GanttFilters} from '@/views/list/helpers/useGanttFilters'
|
||||
import type {GanttFilters} from '@/views/project/helpers/useGanttFilters'
|
||||
|
||||
import {
|
||||
extendDayjs,
|
||||
|
@ -5,7 +5,7 @@
|
||||
<textarea
|
||||
class="add-task-textarea input"
|
||||
:class="{'textarea-empty': newTaskTitle === ''}"
|
||||
:placeholder="$t('list.list.addPlaceholder')"
|
||||
:placeholder="$t('project.list.addPlaceholder')"
|
||||
rows="1"
|
||||
v-focus
|
||||
v-model="newTaskTitle"
|
||||
@ -24,10 +24,10 @@
|
||||
@click="addTask()"
|
||||
icon="plus"
|
||||
:loading="loading"
|
||||
:aria-label="$t('list.list.add')"
|
||||
:aria-label="$t('project.list.add')"
|
||||
>
|
||||
<span class="button-text">
|
||||
{{ $t('list.list.add') }}
|
||||
{{ $t('project.list.add') }}
|
||||
</span>
|
||||
</x-button>
|
||||
</p>
|
||||
@ -107,7 +107,7 @@ const loading = computed(() => taskStore.isLoading)
|
||||
|
||||
async function addTask() {
|
||||
if (newTaskTitle.value === '') {
|
||||
errorMessage.value = t('list.create.addTitleRequired')
|
||||
errorMessage.value = t('project.create.addTitleRequired')
|
||||
return
|
||||
}
|
||||
errorMessage.value = ''
|
||||
@ -128,20 +128,20 @@ async function addTask() {
|
||||
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title) ?? [])
|
||||
await taskStore.ensureLabelsExist(allLabels.flat())
|
||||
|
||||
const newTasks = tasksToCreate.map(async ({title, list}) => {
|
||||
const newTasks = tasksToCreate.map(async ({title, project}) => {
|
||||
if (title === '') {
|
||||
return
|
||||
}
|
||||
|
||||
// If the task has a list specified, make sure to use it
|
||||
let listId = null
|
||||
if (list !== null) {
|
||||
listId = await taskStore.findListId({list, listId: 0})
|
||||
// If the task has a project specified, make sure to use it
|
||||
let projectId = null
|
||||
if (project !== null) {
|
||||
projectId = await taskStore.findProjectId({project, projectId: 0})
|
||||
}
|
||||
|
||||
const task = await taskStore.createNewTask({
|
||||
title,
|
||||
listId: listId || authStore.settings.defaultListId,
|
||||
projectId: projectId || authStore.settings.defaultProjectId,
|
||||
position: props.defaultPosition,
|
||||
})
|
||||
createdTasks[title] = task
|
||||
@ -176,7 +176,7 @@ async function addTask() {
|
||||
}))
|
||||
|
||||
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)
|
||||
|
||||
return rel
|
||||
@ -184,8 +184,8 @@ async function addTask() {
|
||||
await Promise.all(relations)
|
||||
} catch (e: any) {
|
||||
newTaskTitle.value = taskTitleBackup
|
||||
if (e?.message === 'NO_LIST') {
|
||||
errorMessage.value = t('list.create.addListRequired')
|
||||
if (e?.message === 'NO_PROJECT') {
|
||||
errorMessage.value = t('project.create.addProjectRequired')
|
||||
return
|
||||
}
|
||||
throw e
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Multiselect
|
||||
:loading="listUserService.loading"
|
||||
:loading="projectUserService.loading"
|
||||
:placeholder="$t('task.assignee.placeholder')"
|
||||
:multiple="true"
|
||||
@search="findUser"
|
||||
@ -30,7 +30,7 @@ import Multiselect from '@/components/input/multiselect.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {includesById} from '@/helpers/utils'
|
||||
import ListUserService from '@/services/listUsers'
|
||||
import ProjectUserService from '@/services/projectUsers'
|
||||
import {success} from '@/message'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
@ -42,7 +42,7 @@ const props = defineProps({
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
listId: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
@ -59,7 +59,7 @@ const emit = defineEmits(['update:modelValue'])
|
||||
const taskStore = useTaskStore()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const listUserService = shallowReactive(new ListUserService())
|
||||
const projectUserService = shallowReactive(new ProjectUserService())
|
||||
const foundUsers = ref<IUser[]>([])
|
||||
const assignees = ref<IUser[]>([])
|
||||
let isAdding = false
|
||||
@ -94,7 +94,7 @@ async function addAssignee(user: IUser) {
|
||||
async function removeAssignee(user: IUser) {
|
||||
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) {
|
||||
if (assignees.value[a].id === user.id) {
|
||||
assignees.value.splice(a, 1)
|
||||
@ -109,7 +109,7 @@ async function findUser(query: string) {
|
||||
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
|
||||
foundUsers.value = response
|
||||
|
@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<Multiselect
|
||||
class="control is-expanded"
|
||||
:placeholder="$t('list.search')"
|
||||
:search-results="foundLists"
|
||||
:placeholder="$t('project.search')"
|
||||
:search-results="foundProjects"
|
||||
label="title"
|
||||
:select-placeholder="$t('list.searchSelect')"
|
||||
:model-value="list"
|
||||
@update:model-value="Object.assign(list, $event)"
|
||||
:select-placeholder="$t('project.searchSelect')"
|
||||
:model-value="project"
|
||||
@update:model-value="Object.assign(project, $event)"
|
||||
@select="select"
|
||||
@search="findLists"
|
||||
@search="findProjects"
|
||||
>
|
||||
<template #searchResult="{option}">
|
||||
<span class="list-namespace-title search-result">{{ namespace((option as IList).namespaceId) }} ></span>
|
||||
{{ (option as IList).title }}
|
||||
<span class="project-namespace-title search-result">{{ namespace((option as IProject).namespaceId) }} ></span>
|
||||
{{ (option as IProject).title }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
</template>
|
||||
@ -22,19 +22,19 @@ import {reactive, ref, watch} from 'vue'
|
||||
import type {PropType} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<IList>,
|
||||
type: Object as PropType<IProject>,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
@ -42,45 +42,45 @@ const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const list: IList = reactive(new ListModel())
|
||||
const project: IProject = reactive(new ProjectModel())
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newList) => Object.assign(list, newList),
|
||||
(newProject) => Object.assign(project, newProject),
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const foundLists = ref<IList[]>([])
|
||||
function findLists(query: string) {
|
||||
const foundProjects = ref<IProject[]>([])
|
||||
function findProjects(query: string) {
|
||||
if (query === '') {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
Object.assign(list, l)
|
||||
emit('update:modelValue', list)
|
||||
Object.assign(project, l)
|
||||
emit('update:modelValue', project)
|
||||
}
|
||||
|
||||
function namespace(namespaceId: INamespace['id']) {
|
||||
const namespace = namespaceStore.getNamespaceById(namespaceId)
|
||||
return namespace !== null
|
||||
? namespace.title
|
||||
: t('list.shared')
|
||||
: t('project.shared')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-namespace-title {
|
||||
.project-namespace-title {
|
||||
color: var(--grey-500);
|
||||
}
|
||||
</style>
|
@ -37,14 +37,14 @@
|
||||
{{ $t('task.quickAddMagic.multiple') }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('list.list.title') }}</h3>
|
||||
<h3>{{ $t('project.list.title') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.list1', {prefix: prefixes.list}) }}
|
||||
{{ $t('task.quickAddMagic.list2') }}
|
||||
{{ $t('task.quickAddMagic.project1', {prefix: prefixes.project}) }}
|
||||
{{ $t('task.quickAddMagic.project2') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.list3') }}
|
||||
{{ $t('task.quickAddMagic.list4', {prefix: prefixes.list}) }}
|
||||
{{ $t('task.quickAddMagic.project3') }}
|
||||
{{ $t('task.quickAddMagic.project4', {prefix: prefixes.project}) }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('task.quickAddMagic.dateAndTime') }}</h3>
|
||||
|
@ -43,8 +43,8 @@
|
||||
:class="{'is-strikethrough': task.done}"
|
||||
>
|
||||
<span
|
||||
class="different-list"
|
||||
v-if="task.listId !== listId"
|
||||
class="different-project"
|
||||
v-if="task.projectId !== projectId"
|
||||
>
|
||||
<span
|
||||
v-if="task.differentNamespace !== null"
|
||||
@ -52,9 +52,9 @@
|
||||
{{ task.differentNamespace }} >
|
||||
</span>
|
||||
<span
|
||||
v-if="task.differentList !== null"
|
||||
v-tooltip="$t('task.relation.differentList')">
|
||||
{{ task.differentList }} >
|
||||
v-if="task.differentProject !== null"
|
||||
v-tooltip="$t('task.relation.differentProject')">
|
||||
{{ task.differentProject }} >
|
||||
</span>
|
||||
</span>
|
||||
{{ task.title }}
|
||||
@ -98,8 +98,8 @@
|
||||
:class="{ 'is-strikethrough': t.done}"
|
||||
>
|
||||
<span
|
||||
class="different-list"
|
||||
v-if="t.listId !== listId"
|
||||
class="different-project"
|
||||
v-if="t.projectId !== projectId"
|
||||
>
|
||||
<span
|
||||
v-if="t.differentNamespace !== null"
|
||||
@ -107,9 +107,9 @@
|
||||
{{ t.differentNamespace }} >
|
||||
</span>
|
||||
<span
|
||||
v-if="t.differentList !== null"
|
||||
v-tooltip="$t('task.relation.differentList')">
|
||||
{{ t.differentList }} >
|
||||
v-if="t.differentProject !== null"
|
||||
v-tooltip="$t('task.relation.differentProject')">
|
||||
{{ t.differentProject }} >
|
||||
</span>
|
||||
</span>
|
||||
{{ t.title }}
|
||||
@ -186,7 +186,7 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listId: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
@ -230,17 +230,17 @@ async function findTasks(newQuery: string) {
|
||||
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[]) {
|
||||
return tasks.map(task => {
|
||||
// by doing this here once we can save a lot of duplicate calls in the template
|
||||
const {
|
||||
list,
|
||||
project,
|
||||
namespace: taskNamespace,
|
||||
} = getListAndNamespaceById(task.listId) || {list: null, namespace: null}
|
||||
} = getProjectAndNamespaceById(task.projectId) || {project: null, namespace: null}
|
||||
|
||||
return {
|
||||
...task,
|
||||
@ -248,10 +248,10 @@ function mapRelatedTasks(tasks: ITask[]) {
|
||||
(taskNamespace !== null &&
|
||||
taskNamespace.id !== namespace.value.id &&
|
||||
taskNamespace?.title) || null,
|
||||
differentList:
|
||||
(list !== null &&
|
||||
task.listId !== props.listId &&
|
||||
list?.title) || null,
|
||||
differentProject:
|
||||
(project !== null &&
|
||||
task.projectId !== props.projectId &&
|
||||
project?.title) || null,
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -343,7 +343,7 @@ async function removeTaskRelation() {
|
||||
}
|
||||
|
||||
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
|
||||
await addTaskRelation()
|
||||
}
|
||||
@ -351,7 +351,7 @@ async function createAndRelateTask(title: string) {
|
||||
async function toggleTaskDone(task: ITask) {
|
||||
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]) => {
|
||||
return (tasks as ITask[]).some((t, key) => {
|
||||
const found = t.id === task.id
|
||||
@ -379,7 +379,7 @@ async function toggleTaskDone(task: ITask) {
|
||||
}
|
||||
}
|
||||
|
||||
.different-list {
|
||||
.different-project {
|
||||
color: var(--grey-500);
|
||||
width: auto;
|
||||
}
|
||||
|
@ -11,23 +11,23 @@
|
||||
/>
|
||||
|
||||
<ColorBubble
|
||||
v-if="showListColor && listColor !== '' && currentList.id !== task.listId"
|
||||
:color="listColor"
|
||||
v-if="showProjectColor && projectColor !== '' && currentProject.id !== task.projectId"
|
||||
:color="projectColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
|
||||
<div
|
||||
:class="{ 'done': task.done, 'show-list': showList && taskList !== null}"
|
||||
:class="{ 'done': task.done, 'show-project': showProject && project !== null}"
|
||||
class="tasktext"
|
||||
>
|
||||
<span>
|
||||
<router-link
|
||||
v-if="showList && taskList !== null"
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
class="task-list"
|
||||
v-if="showProject && project !== null"
|
||||
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
|
||||
class="task-project"
|
||||
:class="{'mr-2': task.hexColor !== ''}"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})">
|
||||
{{ taskList.title }}
|
||||
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})">
|
||||
{{ project.title }}
|
||||
</router-link>
|
||||
|
||||
<ColorBubble
|
||||
@ -84,13 +84,13 @@
|
||||
<priority-label :priority="task.priority" :done="task.done"/>
|
||||
|
||||
<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"/>
|
||||
</span>
|
||||
<span class="list-task-icon" v-if="task.description">
|
||||
<span class="project-task-icon" v-if="task.description">
|
||||
<icon icon="align-left"/>
|
||||
</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"/>
|
||||
</span>
|
||||
</span>
|
||||
@ -107,12 +107,12 @@
|
||||
</progress>
|
||||
|
||||
<router-link
|
||||
v-if="!showList && currentList.id !== task.listId && taskList !== null"
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
class="task-list"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})"
|
||||
v-if="!showProject && currentProject.id !== task.projectId && project !== null"
|
||||
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
|
||||
class="task-project"
|
||||
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
|
||||
>
|
||||
{{ taskList.title }}
|
||||
{{ project.title }}
|
||||
</router-link>
|
||||
|
||||
<BaseButton
|
||||
@ -151,7 +151,7 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
@ -165,7 +165,7 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showList: {
|
||||
showProject: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
@ -173,7 +173,7 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showListColor: {
|
||||
showProjectColor: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
@ -210,18 +210,18 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
const taskStore = useTaskStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const taskList = computed(() => listStore.getListById(task.value.listId))
|
||||
const listColor = computed(() => taskList.value !== null ? taskList.value.hexColor : '')
|
||||
const project = computed(() => projectStore.getProjectById(task.value.projectId))
|
||||
const projectColor = computed(() => project.value !== null ? project.value.hexColor : '')
|
||||
|
||||
const currentList = computed(() => {
|
||||
return typeof baseStore.currentList === 'undefined' ? {
|
||||
const currentProject = computed(() => {
|
||||
return typeof baseStore.currentProject === 'undefined' ? {
|
||||
id: 0,
|
||||
title: '',
|
||||
} : baseStore.currentList
|
||||
} : baseStore.currentProject
|
||||
})
|
||||
|
||||
const taskDetailRoute = computed(() => ({
|
||||
@ -314,7 +314,7 @@ function hideDeferDueDatePopup(e) {
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
.task-project {
|
||||
width: auto;
|
||||
color: var(--grey-400);
|
||||
font-size: .9rem;
|
||||
@ -329,7 +329,7 @@ function hideDeferDueDatePopup(e) {
|
||||
width: 27px;
|
||||
}
|
||||
|
||||
.list-task-icon {
|
||||
.project-task-icon {
|
||||
margin-left: 6px;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
@ -394,7 +394,7 @@ function hideDeferDueDatePopup(e) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.show-list .parent-tasks {
|
||||
.show-project .parent-tasks {
|
||||
padding-left: .25rem;
|
||||
}
|
||||
|
@ -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 search = ref('')
|
||||
@ -80,7 +80,7 @@ export function useTaskList(listId, sortByDefault: SortBy = SORT_BY_DEFAULT) {
|
||||
loadParams = formatSortOrder(sortBy.value, loadParams)
|
||||
|
||||
return [
|
||||
{listId: listId.value},
|
||||
{projectId: projectId.value},
|
||||
loadParams,
|
||||
page.value || 1,
|
||||
]
|
||||
|
@ -3,7 +3,7 @@ import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
export const getNamespaceTitle = (n: INamespace) => {
|
||||
if (n.id === -1) {
|
||||
return i18n.global.t('namespace.pseudo.sharedLists.title')
|
||||
return i18n.global.t('namespace.pseudo.sharedProjects.title')
|
||||
}
|
||||
if (n.id === -2) {
|
||||
return i18n.global.t('namespace.pseudo.favorites.title')
|
||||
|
@ -1,9 +1,9 @@
|
||||
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) {
|
||||
return i18n.global.t('list.pseudo.favorites.title')
|
||||
return i18n.global.t('project.pseudo.favorites.title')
|
||||
}
|
||||
return l.title
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {getListFromPrefix} from '@/modules/parseTaskText'
|
||||
import {getProjectFromPrefix} from '@/modules/parseTaskText'
|
||||
|
||||
export interface TaskWithParent {
|
||||
title: string,
|
||||
@ -26,7 +26,7 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
|
||||
list: null,
|
||||
}
|
||||
|
||||
task.list = getListFromPrefix(task.title)
|
||||
task.list = getProjectFromPrefix(task.title)
|
||||
|
||||
if (index === 0) {
|
||||
return task
|
||||
@ -49,7 +49,7 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
|
||||
task.parent = task.parent.replace(spaceRegex, '')
|
||||
if (task.list === null) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type {IBucket} from '@/modelTypes/IBucket'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
const key = 'collapsedBuckets'
|
||||
|
||||
@ -13,22 +13,22 @@ function getAllState() {
|
||||
}
|
||||
|
||||
export const saveCollapsedBucketState = (
|
||||
listId: IList['id'],
|
||||
projectId: IProject['id'],
|
||||
collapsedBuckets: CollapsedBuckets,
|
||||
) => {
|
||||
const state = getAllState()
|
||||
state[listId] = collapsedBuckets
|
||||
for (const bucketId in state[listId]) {
|
||||
if (!state[listId][bucketId]) {
|
||||
delete state[listId][bucketId]
|
||||
state[projectId] = collapsedBuckets
|
||||
for (const bucketId in state[projectId]) {
|
||||
if (!state[projectId][bucketId]) {
|
||||
delete state[projectId][bucketId]
|
||||
}
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(state))
|
||||
}
|
||||
|
||||
export function getCollapsedBucketState(listId : IList['id']) {
|
||||
export function getCollapsedBucketState(projectId : IProject['id']) {
|
||||
const state = getAllState()
|
||||
return typeof state[listId] !== 'undefined'
|
||||
? state[listId]
|
||||
return typeof state[projectId] !== 'undefined'
|
||||
? state[projectId]
|
||||
: {}
|
||||
}
|
||||
|
@ -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.')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!listId) {
|
||||
if (!projectId) {
|
||||
return
|
||||
}
|
||||
|
||||
// We use local storage and not the store here to make it persistent across reloads.
|
||||
const savedListView = localStorage.getItem('listView')
|
||||
let savedListViewJson: ListView | false = false
|
||||
if (savedListView !== null) {
|
||||
savedListViewJson = JSON.parse(savedListView) as ListView
|
||||
const savedProjectView = localStorage.getItem(PROJECT_VIEW_SETTINGS_KEY)
|
||||
let savedProjectViewJson: ProjectView | false = false
|
||||
if (savedProjectView !== null) {
|
||||
savedProjectViewJson = JSON.parse(savedProjectView) as ProjectView
|
||||
}
|
||||
|
||||
let listView: ListView = {}
|
||||
if (savedListViewJson) {
|
||||
listView = savedListViewJson
|
||||
let projectView: ProjectView = {}
|
||||
if (savedProjectViewJson) {
|
||||
projectView = savedProjectViewJson
|
||||
}
|
||||
|
||||
listView[listId] = routeName
|
||||
localStorage.setItem('listView', JSON.stringify(listView))
|
||||
projectView[projectId] = routeName
|
||||
localStorage.setItem(PROJECT_VIEW_SETTINGS_KEY, JSON.stringify(projectView))
|
||||
}
|
||||
|
||||
export const getListView = (listId: IList['id']) => {
|
||||
// Remove old stored settings
|
||||
const savedListView = localStorage.getItem('listView')
|
||||
if (savedListView !== null && savedListView.startsWith('list.')) {
|
||||
export const getProjectView = (projectId: IProject['id']) => {
|
||||
// Migrate old setting over
|
||||
// TODO: remove when 1.0 release
|
||||
const oldListViewSettings = localStorage.getItem('listView')
|
||||
if (oldListViewSettings !== null) {
|
||||
localStorage.setItem(PROJECT_VIEW_SETTINGS_KEY, oldListViewSettings)
|
||||
localStorage.removeItem('listView')
|
||||
}
|
||||
|
||||
if (!savedListView) {
|
||||
return DEFAULT_LIST_VIEW
|
||||
|
||||
// Remove old stored settings
|
||||
// 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 (!savedListViewJson[listId]) {
|
||||
return DEFAULT_LIST_VIEW
|
||||
if (!savedProjectView) {
|
||||
return DEFAULT_PROJECT_VIEW
|
||||
}
|
||||
|
||||
return savedListViewJson[listId]
|
||||
const savedProjectViewJson: ProjectView = JSON.parse(savedProjectView)
|
||||
|
||||
if (!savedProjectViewJson[projectId]) {
|
||||
return DEFAULT_PROJECT_VIEW
|
||||
}
|
||||
|
||||
return savedProjectViewJson[projectId]
|
||||
}
|
@ -5,10 +5,10 @@
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "New list",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"project": {
|
||||
"newText": "You can create a new project for your new tasks:",
|
||||
"new": "New project",
|
||||
"importText": "Or import your projects and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
},
|
||||
@ -85,7 +85,7 @@
|
||||
"weekStartSunday": "Sunday",
|
||||
"weekStartMonday": "Monday",
|
||||
"language": "Language",
|
||||
"defaultList": "Default List",
|
||||
"defaultProject": "Default Project",
|
||||
"timezone": "Time Zone",
|
||||
"overdueTasksRemindersTime": "Overdue tasks reminder email time"
|
||||
},
|
||||
@ -143,7 +143,7 @@
|
||||
},
|
||||
"deletion": {
|
||||
"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.",
|
||||
"confirm": "Delete my account",
|
||||
"requestSuccess": "The request was successful. You'll receive an email with further instructions.",
|
||||
@ -157,39 +157,39 @@
|
||||
},
|
||||
"export": {
|
||||
"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:",
|
||||
"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.",
|
||||
"downloadTitle": "Download your exported Vikunja data"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"archived": "This list is archived. It is not possible to create new or edit tasks for it.",
|
||||
"title": "List Title",
|
||||
"project": {
|
||||
"archived": "This project is archived. It is not possible to create new or edit tasks for it.",
|
||||
"title": "Project Title",
|
||||
"color": "Color",
|
||||
"lists": "Lists",
|
||||
"search": "Type to search for a list…",
|
||||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"noDescriptionAvailable": "No list description is available.",
|
||||
"projects": "Projects",
|
||||
"search": "Type to search for a project…",
|
||||
"searchSelect": "Click or press enter to select this project",
|
||||
"shared": "Shared Projects",
|
||||
"noDescriptionAvailable": "No project description is available.",
|
||||
"create": {
|
||||
"header": "New list",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"header": "New project",
|
||||
"titlePlaceholder": "The project's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
"addListRequired": "Please specify a list or set a default list in the settings."
|
||||
"createdSuccess": "The project was successfully created.",
|
||||
"addProjectRequired": "Please specify a project or set a default project in the settings."
|
||||
},
|
||||
"archive": {
|
||||
"title": "Archive \"{list}\"",
|
||||
"archive": "Archive this list",
|
||||
"unarchive": "Un-Archive this list",
|
||||
"title": "Archive \"{project}\"",
|
||||
"archive": "Archive this project",
|
||||
"unarchive": "Un-Archive this project",
|
||||
"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.",
|
||||
"success": "The list was successfully archived."
|
||||
"archiveText": "You won't be able to edit this project or create new tasks until you un-archive it.",
|
||||
"success": "The project was successfully archived."
|
||||
},
|
||||
"background": {
|
||||
"title": "Set list background",
|
||||
"title": "Set project background",
|
||||
"remove": "Remove Background",
|
||||
"upload": "Choose a background from your pc",
|
||||
"searchPlaceholder": "Search for a background…",
|
||||
@ -199,40 +199,40 @@
|
||||
"removeSuccess": "The background has been removed successfully!"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete \"{list}\"",
|
||||
"header": "Delete this list",
|
||||
"text1": "Are you sure you want to delete this list and all of its contents?",
|
||||
"title": "Delete \"{project}\"",
|
||||
"header": "Delete this project",
|
||||
"text1": "Are you sure you want to delete this project and all of its contents?",
|
||||
"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.",
|
||||
"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": {
|
||||
"title": "Duplicate this list",
|
||||
"title": "Duplicate this project",
|
||||
"label": "Duplicate",
|
||||
"text": "Select a namespace which should hold the duplicated list:",
|
||||
"success": "The list was successfully duplicated."
|
||||
"text": "Select a namespace which should hold the duplicated project:",
|
||||
"success": "The project was successfully duplicated."
|
||||
},
|
||||
"edit": {
|
||||
"header": "Edit This List",
|
||||
"title": "Edit \"{list}\"",
|
||||
"titlePlaceholder": "The list 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.",
|
||||
"identifier": "List Identifier",
|
||||
"identifierPlaceholder": "The list identifier goes here…",
|
||||
"header": "Edit This Project",
|
||||
"title": "Edit \"{project}\"",
|
||||
"titlePlaceholder": "The project title goes here…",
|
||||
"identifierTooltip": "The project identifier can be used to uniquely identify a task across projects. You can set it to empty to disable it.",
|
||||
"identifier": "Project Identifier",
|
||||
"identifierPlaceholder": "The project identifier goes here…",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "The lists description goes here…",
|
||||
"descriptionPlaceholder": "The projects description goes here…",
|
||||
"color": "Color",
|
||||
"success": "The list was successfully updated."
|
||||
"success": "The project was successfully updated."
|
||||
},
|
||||
"share": {
|
||||
"header": "Share this list",
|
||||
"title": "Share \"{list}\"",
|
||||
"header": "Share this project",
|
||||
"title": "Share \"{project}\"",
|
||||
"share": "Share",
|
||||
"links": {
|
||||
"title": "Share Links",
|
||||
"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",
|
||||
"name": "Name (optional)",
|
||||
"namePlaceholder": "e.g. Lorem Ipsum",
|
||||
@ -241,7 +241,7 @@
|
||||
"passwordExplanation": "When authenticating, the user will be required to enter this password.",
|
||||
"noName": "No name set",
|
||||
"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.",
|
||||
"deleteSuccess": "The link share was successfully deleted",
|
||||
"view": "View",
|
||||
@ -274,7 +274,7 @@
|
||||
"title": "List",
|
||||
"add": "Add",
|
||||
"addPlaceholder": "Add a new task…",
|
||||
"empty": "This list is currently empty.",
|
||||
"empty": "This project is currently empty.",
|
||||
"newTaskCta": "Create a new task.",
|
||||
"editTask": "Edit Task"
|
||||
},
|
||||
@ -322,36 +322,36 @@
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"title": "Namespaces & Lists",
|
||||
"title": "Namespaces & Projects",
|
||||
"namespace": "Namespace",
|
||||
"showArchived": "Show Archived",
|
||||
"noneAvailable": "You don't have any namespaces right now.",
|
||||
"unarchive": "Un-Archive",
|
||||
"archived": "Archived",
|
||||
"noLists": "This namespace does not contain any lists.",
|
||||
"createList": "Create a new list in this namespace.",
|
||||
"noProjects": "This namespace does not contain any projects.",
|
||||
"createProject": "Create a new project in this namespace.",
|
||||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"create": {
|
||||
"title": "New namespace",
|
||||
"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?",
|
||||
"success": "The namespace was successfully created."
|
||||
},
|
||||
"archive": {
|
||||
"titleArchive": "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.",
|
||||
"unarchiveText": "You will be able to create new lists or edit it.",
|
||||
"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 projects or edit it.",
|
||||
"success": "The namespace was successfully 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": {
|
||||
"title": "Delete \"{namespace}\"",
|
||||
"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."
|
||||
},
|
||||
"edit": {
|
||||
@ -371,8 +371,8 @@
|
||||
"isArchived": "This namespace is archived"
|
||||
},
|
||||
"pseudo": {
|
||||
"sharedLists": {
|
||||
"title": "Shared Lists"
|
||||
"sharedProjects": {
|
||||
"title": "Shared Projects"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favorites"
|
||||
@ -403,7 +403,7 @@
|
||||
},
|
||||
"create": {
|
||||
"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",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
@ -435,7 +435,7 @@
|
||||
"label": {
|
||||
"title": "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.",
|
||||
"search": "Type to search for a label…",
|
||||
"create": {
|
||||
@ -460,7 +460,7 @@
|
||||
},
|
||||
"sharing": {
|
||||
"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.",
|
||||
"invalidPassword": "The password is invalid."
|
||||
},
|
||||
@ -529,7 +529,7 @@
|
||||
"code": "Code",
|
||||
"quote": "Quote",
|
||||
"unorderedList": "Unordered List",
|
||||
"orderedList": "Ordered List",
|
||||
"orderedList ": "Ordered List",
|
||||
"cleanBlock": "Clean Block",
|
||||
"link": "Link",
|
||||
"image": "Image",
|
||||
@ -622,7 +622,7 @@
|
||||
"chooseDueDate": "Click here to set a due date",
|
||||
"chooseStartDate": "Click here to set a start 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!",
|
||||
"undone": "Mark as undone",
|
||||
"created": "Created {0} by {1}",
|
||||
@ -630,7 +630,7 @@
|
||||
"doneAt": "Done {0}",
|
||||
"updateSuccess": "The task was saved 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}",
|
||||
"closePopup": "Close popup",
|
||||
"delete": {
|
||||
@ -650,7 +650,7 @@
|
||||
"percentDone": "Set Progress",
|
||||
"attachments": "Add Attachments",
|
||||
"relatedTasks": "Add Relation",
|
||||
"moveList": "Move",
|
||||
"moveProject": "Move",
|
||||
"color": "Set Color",
|
||||
"delete": "Delete",
|
||||
"favorite": "Add to Favorites",
|
||||
@ -677,21 +677,21 @@
|
||||
"updated": "Updated"
|
||||
},
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
|
||||
"subscribedProject": "You are currently subscribed to this project and will 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.",
|
||||
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
|
||||
"subscribe": "Subscribe",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
|
||||
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
|
||||
"subscribeSuccessList": "You are now subscribed to this list",
|
||||
"unsubscribeSuccessList": "You are now unsubscribed to this list",
|
||||
"subscribeSuccessProject": "You are now subscribed to this project",
|
||||
"unsubscribeSuccessProject": "You are now unsubscribed to this project",
|
||||
"subscribeSuccessTask": "You are now subscribed to this task",
|
||||
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
|
||||
},
|
||||
@ -765,7 +765,7 @@
|
||||
"new": "New Task Relation",
|
||||
"searchPlaceholder": "Type search for a new task to add as related…",
|
||||
"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.",
|
||||
"noneYet": "No task relations yet.",
|
||||
"delete": "Delete Task Relation",
|
||||
@ -815,10 +815,10 @@
|
||||
"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.",
|
||||
"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}.",
|
||||
"list2": "This will return an error if the list does not exist.",
|
||||
"list3": "To use spaces, simply add a \" or ' around the list name.",
|
||||
"list4": "For example: {prefix}\"List with spaces\".",
|
||||
"project1": "To set a project for the task to appear in, enter its name prefixed with {prefix}.",
|
||||
"project2": "This will return an error if the project does not exist.",
|
||||
"project3": "To use spaces, simply add a \" or ' around the project name.",
|
||||
"project4": "For example: {prefix}\"Project with spaces\".",
|
||||
"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:",
|
||||
"dateWeekday": "any weekday, will use the next date with that date",
|
||||
@ -851,19 +851,19 @@
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"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."
|
||||
},
|
||||
"deleteUser": {
|
||||
"header": "Remove a 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."
|
||||
},
|
||||
"leave": {
|
||||
"title": "Leave 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."
|
||||
}
|
||||
},
|
||||
@ -895,13 +895,13 @@
|
||||
"attachment": "Add an attachment to this task",
|
||||
"related": "Modify related tasks 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",
|
||||
"description": "Toggle editing of the task description"
|
||||
},
|
||||
"list": {
|
||||
"title": "List Views",
|
||||
"switchToListView": "Switch to list view",
|
||||
"project": {
|
||||
"title": "Project Views",
|
||||
"switchToListView": "Switch to project view",
|
||||
"switchToGanttView": "Switch to gantt view",
|
||||
"switchToKanbanView": "Switch to kanban view",
|
||||
"switchToTableView": "Switch to table view"
|
||||
@ -910,7 +910,7 @@
|
||||
"title": "Navigation",
|
||||
"overview": "Navigate to overview",
|
||||
"upcoming": "Navigate to upcoming tasks",
|
||||
"namespaces": "Navigate to namespaces & lists",
|
||||
"namespaces": "Navigate to namespaces & projects",
|
||||
"labels": "Navigate to labels",
|
||||
"teams": "Navigate to teams"
|
||||
}
|
||||
@ -927,7 +927,7 @@
|
||||
"unarchive": "Un-Archive",
|
||||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newList": "New list"
|
||||
"newProject": "New project"
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
@ -946,24 +946,24 @@
|
||||
"notification": {
|
||||
"title": "Notifications",
|
||||
"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": {
|
||||
"commands": "Commands",
|
||||
"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",
|
||||
"lists": "Lists",
|
||||
"projects": "Projects",
|
||||
"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…",
|
||||
"newNamespace": "Enter the title of the new namespace…",
|
||||
"newTeam": "Enter the name of the new team…",
|
||||
"createTask": "Create a task in the current list ({title})",
|
||||
"createList": "Create a list in the current namespace ({title})",
|
||||
"createTask": "Create a task in the current project ({title})",
|
||||
"createProject": "Create a project in the current namespace ({title})",
|
||||
"cmds": {
|
||||
"newTask": "New task",
|
||||
"newList": "New list",
|
||||
"newProject": "New project",
|
||||
"newNamespace": "New namespace",
|
||||
"newTeam": "New team"
|
||||
}
|
||||
@ -995,15 +995,15 @@
|
||||
"1018": "The user avatar type setting is invalid.",
|
||||
"2001": "ID cannot be empty or 0.",
|
||||
"2002": "Some of the request data was invalid.",
|
||||
"3001": "The list does not exist.",
|
||||
"3004": "You need to have read permissions on that list to perform that action.",
|
||||
"3005": "The list title cannot be empty.",
|
||||
"3006": "The list share does not exist.",
|
||||
"3007": "A list 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.",
|
||||
"4001": "The list task text cannot be empty.",
|
||||
"4002": "The list task does not exist.",
|
||||
"4003": "All bulk editing tasks must belong to the same list.",
|
||||
"3001": "The project does not exist.",
|
||||
"3004": "You need to have read permissions on that project to perform that action.",
|
||||
"3005": "The project title cannot be empty.",
|
||||
"3006": "The project share does not exist.",
|
||||
"3007": "A project with this identifier already exists.",
|
||||
"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 project task text cannot be empty.",
|
||||
"4002": "The project task does not exist.",
|
||||
"4003": "All bulk editing tasks must belong to the same project.",
|
||||
"4004": "Need at least one task when bulk editing tasks.",
|
||||
"4005": "You do not have the right to see the task.",
|
||||
"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.",
|
||||
"6001": "The team name cannot be empty.",
|
||||
"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.",
|
||||
"6006": "Cannot delete the last team member.",
|
||||
"6007": "The team does not have access to the list to perform that action.",
|
||||
"7002": "The user already has access to that list.",
|
||||
"7003": "You do not have access to that list.",
|
||||
"6007": "The team does not have access to the project to perform that action.",
|
||||
"7002": "The user already has access to that project.",
|
||||
"7003": "You do not have access to that project.",
|
||||
"8001": "This label already exists on that task.",
|
||||
"8002": "The label does not exist.",
|
||||
"8003": "You do not have access to this label.",
|
||||
"9001": "The right is invalid.",
|
||||
"10001": "The bucket does not exist.",
|
||||
"10002": "The bucket does not belong to that list.",
|
||||
"10003": "You cannot remove the last bucket on a list.",
|
||||
"10002": "The bucket does not belong to that project.",
|
||||
"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.",
|
||||
"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.",
|
||||
"11002": "Saved filters are not available for link shares.",
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
|
@ -5,7 +5,7 @@ import type {ITask} from './ITask'
|
||||
export interface IBucket extends IAbstract {
|
||||
id: number
|
||||
title: string
|
||||
listId: number
|
||||
projectId: number
|
||||
limit: number
|
||||
tasks: ITask[]
|
||||
isDoneBucket: boolean
|
||||
|
@ -7,7 +7,7 @@ export interface ILabel extends IAbstract {
|
||||
hexColor: string
|
||||
description: string
|
||||
createdBy: IUser
|
||||
listId: number
|
||||
projectId: number
|
||||
textColor: string
|
||||
|
||||
created: Date
|
||||
|
@ -8,7 +8,7 @@ export interface ILinkShare extends IAbstract {
|
||||
right: Right
|
||||
sharedBy: IUser
|
||||
sharingType: number // FIXME: use correct numbers
|
||||
listId: number
|
||||
projectId: number
|
||||
name: string
|
||||
password: string
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type {IAbstract} from './IAbstract'
|
||||
import type {IList} from './IList'
|
||||
import type {IProject} from './IProject'
|
||||
import type {IUser} from './IUser'
|
||||
import type {ISubscription} from './ISubscription'
|
||||
|
||||
@ -8,7 +8,7 @@ export interface INamespace extends IAbstract {
|
||||
title: string
|
||||
description: string
|
||||
owner: IUser
|
||||
lists: IList[]
|
||||
projects: IProject[]
|
||||
isArchived: boolean
|
||||
hexColor: string
|
||||
subscription: ISubscription
|
||||
|
@ -3,13 +3,13 @@ import type {IUser} from './IUser'
|
||||
import type {ITask} from './ITask'
|
||||
import type {ITaskComment} from './ITaskComment'
|
||||
import type {ITeam} from './ITeam'
|
||||
import type { IList } from './IList'
|
||||
import type { IProject } from './IProject'
|
||||
|
||||
export const NOTIFICATION_NAMES = {
|
||||
'TASK_COMMENT': 'task.comment',
|
||||
'TASK_ASSIGNED': 'task.assigned',
|
||||
'TASK_DELETED': 'task.deleted',
|
||||
'LIST_CREATED': 'list.created',
|
||||
'PROJECT_CREATED': 'project.created',
|
||||
'TEAM_MEMBER_ADDED': 'team.member.added',
|
||||
} as const
|
||||
|
||||
@ -32,7 +32,7 @@ interface NotificationDeleted extends Notification {
|
||||
|
||||
interface NotificationCreated extends Notification {
|
||||
task: ITask
|
||||
list: IList
|
||||
project: IProject
|
||||
}
|
||||
|
||||
interface NotificationMemberAdded extends Notification {
|
||||
|
@ -5,7 +5,7 @@ import type {ISubscription} from './ISubscription'
|
||||
import type {INamespace} from './INamespace'
|
||||
|
||||
|
||||
export interface IList extends IAbstract {
|
||||
export interface IProject extends IAbstract {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
@ -1,9 +1,9 @@
|
||||
import type {IAbstract} from './IAbstract'
|
||||
import type {IList} from './IList'
|
||||
import type {IProject} from './IProject'
|
||||
import type {INamespace} from './INamespace'
|
||||
|
||||
export interface IListDuplicate extends IAbstract {
|
||||
listId: number
|
||||
export interface IProjectDuplicate extends IAbstract {
|
||||
projectId: number
|
||||
namespaceId: INamespace['id']
|
||||
list: IList
|
||||
project: IProject
|
||||
}
|
@ -5,7 +5,7 @@ import type {IUser} from './IUser'
|
||||
import type {ILabel} from './ILabel'
|
||||
import type {IAttachment} from './IAttachment'
|
||||
import type {ISubscription} from './ISubscription'
|
||||
import type {IList} from './IList'
|
||||
import type {IProject} from './IProject'
|
||||
import type {IBucket} from './IBucket'
|
||||
|
||||
import type {IRelationKind} from '@/types/IRelationKind'
|
||||
@ -49,7 +49,7 @@ export interface ITask extends IAbstract {
|
||||
created: 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']
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type {IUser} from './IUser'
|
||||
import type {IList} from './IList'
|
||||
import type {IProject} from './IProject'
|
||||
|
||||
export interface ITeamMember extends IUser {
|
||||
admin: boolean
|
||||
teamId: IList['id']
|
||||
teamId: IProject['id']
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type {ITeamShareBase} from './ITeamShareBase'
|
||||
import type {IList} from './IList'
|
||||
import type {IProject} from './IProject'
|
||||
|
||||
export interface ITeamList extends ITeamShareBase {
|
||||
listId: IList['id']
|
||||
export interface ITeamProject extends ITeamShareBase {
|
||||
projectId: IProject['id']
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import type {IUserShareBase} from './IUserShareBase'
|
||||
import type {IList} from './IList'
|
||||
import type {IProject} from './IProject'
|
||||
|
||||
export interface IUserList extends IUserShareBase {
|
||||
listId: IList['id']
|
||||
export interface IUserProject extends IUserShareBase {
|
||||
projectId: IProject['id']
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
|
||||
import type {IAbstract} from './IAbstract'
|
||||
import type {IList} from './IList'
|
||||
import type {IProject} from './IProject'
|
||||
|
||||
export interface IUserSettings extends IAbstract {
|
||||
name: string
|
||||
@ -9,7 +9,7 @@ export interface IUserSettings extends IAbstract {
|
||||
discoverableByEmail: boolean
|
||||
overdueTasksRemindersEnabled: boolean
|
||||
overdueTasksRemindersTime: any
|
||||
defaultListId: undefined | IList['id']
|
||||
defaultProjectId: undefined | IProject['id']
|
||||
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
timezone: string
|
||||
language: string
|
||||
|
@ -9,7 +9,7 @@ import type {IUser} from '@/modelTypes/IUser'
|
||||
export default class BucketModel extends AbstractModel<IBucket> implements IBucket {
|
||||
id = 0
|
||||
title = ''
|
||||
listId = ''
|
||||
projectId = ''
|
||||
limit = 0
|
||||
tasks: ITask[] = []
|
||||
isDoneBucket = false
|
||||
|
@ -16,7 +16,7 @@ export default class LabelModel extends AbstractModel<ILabel> implements ILabel
|
||||
hexColor = DEFAULT_LABEL_BACKGROUND_COLOR
|
||||
description = ''
|
||||
createdBy: IUser
|
||||
listId = 0
|
||||
projectId = 0
|
||||
textColor = ''
|
||||
|
||||
created: Date = null
|
||||
|
@ -11,7 +11,7 @@ export default class LinkShareModel extends AbstractModel<ILinkShare> implements
|
||||
right: Right = RIGHTS.READ
|
||||
sharedBy: IUser = UserModel
|
||||
sharingType = 0 // FIXME: use correct numbers
|
||||
listId = 0
|
||||
projectId = 0
|
||||
name: ''
|
||||
password: ''
|
||||
created: Date = null
|
||||
|
@ -1,11 +1,11 @@
|
||||
import AbstractModel from './abstractModel'
|
||||
import ListModel from './list'
|
||||
import ProjectModel from './project'
|
||||
import UserModel from './user'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
|
||||
export default class NamespaceModel extends AbstractModel<INamespace> implements INamespace {
|
||||
@ -13,7 +13,7 @@ export default class NamespaceModel extends AbstractModel<INamespace> implements
|
||||
title = ''
|
||||
description = ''
|
||||
owner: IUser = UserModel
|
||||
lists: IList[] = []
|
||||
projects: IProject[] = []
|
||||
isArchived = false
|
||||
hexColor = ''
|
||||
subscription: ISubscription = null
|
||||
@ -29,8 +29,8 @@ export default class NamespaceModel extends AbstractModel<INamespace> implements
|
||||
this.hexColor = '#' + this.hexColor
|
||||
}
|
||||
|
||||
this.lists = this.lists.map(l => {
|
||||
return new ListModel(l)
|
||||
this.projects = this.projects.map(l => {
|
||||
return new ProjectModel(l)
|
||||
})
|
||||
|
||||
this.owner = new UserModel(this.owner)
|
||||
|
@ -3,7 +3,7 @@ import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
import UserModel, {getDisplayName} from '@/models/user'
|
||||
import TaskModel from '@/models/task'
|
||||
import TaskCommentModel from '@/models/taskComment'
|
||||
import ListModel from '@/models/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
import TeamModel from '@/models/team'
|
||||
|
||||
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),
|
||||
}
|
||||
break
|
||||
case NOTIFICATION_NAMES.LIST_CREATED:
|
||||
case NOTIFICATION_NAMES.PROJECT_CREATED:
|
||||
this.notification = {
|
||||
doer: new UserModel(this.notification.doer),
|
||||
list: new ListModel(this.notification.list),
|
||||
project: new ProjectModel(this.notification.project),
|
||||
}
|
||||
break
|
||||
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()}`
|
||||
case NOTIFICATION_NAMES.TASK_DELETED:
|
||||
return `deleted ${this.notification.task.getTextIdentifier()}`
|
||||
case NOTIFICATION_NAMES.LIST_CREATED:
|
||||
return `created ${this.notification.list.title}`
|
||||
case NOTIFICATION_NAMES.PROJECT_CREATED:
|
||||
return `created ${this.notification.project.title}`
|
||||
case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED:
|
||||
who = `${getDisplayName(this.notification.member)}`
|
||||
|
||||
|
@ -3,13 +3,13 @@ import TaskModel from '@/models/task'
|
||||
import UserModel from '@/models/user'
|
||||
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 {ITask} from '@/modelTypes/ITask'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
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
|
||||
title = ''
|
||||
description = ''
|
||||
@ -28,7 +28,7 @@ export default class ListModel extends AbstractModel<IList> implements IList {
|
||||
created: Date = null
|
||||
updated: Date = null
|
||||
|
||||
constructor(data: Partial<IList> = {}) {
|
||||
constructor(data: Partial<IProject> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
|
@ -1,19 +1,19 @@
|
||||
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 {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
export default class ListDuplicateModel extends AbstractModel<IListDuplicate> implements IListDuplicate {
|
||||
listId = 0
|
||||
export default class ProjectDuplicateModel extends AbstractModel<IProjectDuplicate> implements IProjectDuplicate {
|
||||
projectId = 0
|
||||
namespaceId: INamespace['id'] = 0
|
||||
list: IList = ListModel
|
||||
project: IProject = ProjectModel
|
||||
|
||||
constructor(data : Partial<IListDuplicate>) {
|
||||
constructor(data : Partial<IProjectDuplicate>) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
|
||||
this.list = new ListModel(this.list)
|
||||
this.project = new ProjectModel(this.project)
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
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 {IBucket} from '@/modelTypes/IBucket'
|
||||
|
||||
@ -93,7 +93,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||
created: Date = null
|
||||
updated: Date = null
|
||||
|
||||
listId: IList['id'] = 0
|
||||
projectId: IProject['id'] = 0
|
||||
bucketId: IBucket['id'] = 0
|
||||
|
||||
constructor(data: Partial<ITask> = {}) {
|
||||
@ -142,7 +142,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||
// Make all attachments to attachment models
|
||||
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}`) {
|
||||
this.identifier = ''
|
||||
}
|
||||
@ -155,7 +155,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||
this.created = new Date(this.created)
|
||||
this.updated = new Date(this.updated)
|
||||
|
||||
this.listId = Number(this.listId)
|
||||
this.projectId = Number(this.projectId)
|
||||
}
|
||||
|
||||
getTextIdentifier() {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import UserModel from './user'
|
||||
|
||||
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 {
|
||||
admin = false
|
||||
teamId: IList['id'] = 0
|
||||
teamId: IProject['id'] = 0
|
||||
|
||||
constructor(data: Partial<ITeamMember>) {
|
||||
super(data)
|
||||
|
@ -1,12 +1,12 @@
|
||||
import TeamShareBaseModel from './teamShareBase'
|
||||
|
||||
import type {ITeamList} from '@/modelTypes/ITeamList'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {ITeamProject} from '@/modelTypes/ITeamProject'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
export default class TeamListModel extends TeamShareBaseModel implements ITeamList {
|
||||
listId: IList['id'] = 0
|
||||
export default class TeamProjectModel extends TeamShareBaseModel implements ITeamProject {
|
||||
projectId: IProject['id'] = 0
|
||||
|
||||
constructor(data: Partial<ITeamList>) {
|
||||
constructor(data: Partial<ITeamProject>) {
|
||||
super(data)
|
||||
this.assignData(data)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import type {ITeam} from '@/modelTypes/ITeam'
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
teamId: ITeam['id'] = 0
|
||||
|
@ -1,13 +1,13 @@
|
||||
import UserShareBaseModel from './userShareBase'
|
||||
|
||||
import type {IUserList} from '@/modelTypes/IUserList'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IUserProject} from '@/modelTypes/IUserProject'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
// This class extends the user share model with a 'rights' parameter which is used in sharing
|
||||
export default class UserListModel extends UserShareBaseModel implements IUserList {
|
||||
listId: IList['id'] = 0
|
||||
export default class UserProjectModel extends UserShareBaseModel implements IUserProject {
|
||||
projectId: IProject['id'] = 0
|
||||
|
||||
constructor(data: Partial<IUserList>) {
|
||||
constructor(data: Partial<IUserProject>) {
|
||||
super(data)
|
||||
this.assignData(data)
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
|
||||
discoverableByEmail = false
|
||||
overdueTasksRemindersEnabled = true
|
||||
overdueTasksRemindersTime = undefined
|
||||
defaultListId = undefined
|
||||
defaultProjectId = undefined
|
||||
weekStart = 0 as IUserSettings['weekStart']
|
||||
timezone = ''
|
||||
language = getCurrentLanguage()
|
||||
|
@ -21,14 +21,14 @@ describe('Parse Task Text', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
expect(result.text).toBe(text)
|
||||
})
|
||||
|
||||
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')
|
||||
const now = new Date()
|
||||
@ -37,7 +37,7 @@ describe('Parse Task Text', () => {
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
expect(result.labels).toHaveLength(1)
|
||||
expect(result.labels[0]).toBe('label')
|
||||
expect(result.list).toBe('list')
|
||||
expect(result.project).toBe('project')
|
||||
expect(result.priority).toBe(2)
|
||||
expect(result.assignees).toHaveLength(1)
|
||||
expect(result.assignees[0]).toBe('user')
|
||||
@ -574,36 +574,36 @@ describe('Parse Task Text', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('List', () => {
|
||||
it('should parse a list', () => {
|
||||
const result = parseTaskText('Lorem Ipsum +list')
|
||||
describe('Project', () => {
|
||||
it('should parse a project', () => {
|
||||
const result = parseTaskText('Lorem Ipsum +project')
|
||||
|
||||
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', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +'list with long name'`)
|
||||
it('should parse a project with a space in it', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +'project with long name'`)
|
||||
|
||||
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 "', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +"list with long name"`)
|
||||
it('should parse a project with a space in it and "', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +"project with long name"`)
|
||||
|
||||
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', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +list1 +list2 +list3`)
|
||||
it('should parse only the first project', () => {
|
||||
const result = parseTaskText(`Lorem Ipsum +project1 +project2 +project3`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum +list2 +list3')
|
||||
expect(result.list).toBe('list1')
|
||||
expect(result.text).toBe('Lorem Ipsum +project2 +project3')
|
||||
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`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.list).toBe('today')
|
||||
expect(result.project).toBe('today')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -5,14 +5,14 @@ import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||
|
||||
const VIKUNJA_PREFIXES: Prefixes = {
|
||||
label: '*',
|
||||
list: '+',
|
||||
project: '+',
|
||||
priority: '!',
|
||||
assignee: '@',
|
||||
}
|
||||
|
||||
const TODOIST_PREFIXES: Prefixes = {
|
||||
label: '@',
|
||||
list: '#',
|
||||
project: '#',
|
||||
priority: '!',
|
||||
assignee: '+',
|
||||
}
|
||||
@ -38,7 +38,7 @@ export interface ParsedTaskText {
|
||||
text: string,
|
||||
date: Date | null,
|
||||
labels: string[],
|
||||
list: string | null,
|
||||
project: string | null,
|
||||
priority: number | null,
|
||||
assignees: string[],
|
||||
repeats: IRepeatAfter | null,
|
||||
@ -46,13 +46,13 @@ export interface ParsedTaskText {
|
||||
|
||||
interface Prefixes {
|
||||
label: string,
|
||||
list: string,
|
||||
project: string,
|
||||
priority: 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
|
||||
*/
|
||||
@ -61,7 +61,7 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
|
||||
text: text,
|
||||
date: null,
|
||||
labels: [],
|
||||
list: null,
|
||||
project: null,
|
||||
priority: null,
|
||||
assignees: [],
|
||||
repeats: null,
|
||||
@ -75,8 +75,8 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
|
||||
result.labels = getLabelsFromPrefix(text, prefixes.label) ?? []
|
||||
result.text = cleanupItemText(result.text, result.labels, prefixes.label)
|
||||
|
||||
result.list = getListFromPrefix(result.text, prefixes.list)
|
||||
result.text = result.list !== null ? cleanupItemText(result.text, [result.list], prefixes.list) : result.text
|
||||
result.project = getProjectFromPrefix(result.text, prefixes.project)
|
||||
result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
|
||||
|
||||
result.priority = getPriority(result.text, prefixes.priority)
|
||||
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))
|
||||
}
|
||||
|
||||
export const getListFromPrefix = (text: string, listPrefix: string | null = null): string | null => {
|
||||
if (listPrefix === null) {
|
||||
export const getProjectFromPrefix = (text: string, projectPrefix: string | null = null): string | null => {
|
||||
if (projectPrefix === null) {
|
||||
const prefixes = PREFIXES[getQuickAddMagicMode()]
|
||||
if (prefixes === undefined) {
|
||||
return null
|
||||
}
|
||||
listPrefix = prefixes.list
|
||||
projectPrefix = prefixes.project
|
||||
}
|
||||
const lists: string[] = getItemsFromPrefix(text, listPrefix)
|
||||
return lists.length > 0 ? lists[0] : null
|
||||
const projects: string[] = getItemsFromPrefix(text, projectPrefix)
|
||||
return projects.length > 0 ? projects[0] : null
|
||||
}
|
||||
|
||||
export const getLabelsFromPrefix = (text: string, listPrefix: string | null = null): string[] | null => {
|
||||
if (listPrefix === null) {
|
||||
export const getLabelsFromPrefix = (text: string, projectPrefix: string | null = null): string[] | null => {
|
||||
if (projectPrefix === null) {
|
||||
const prefixes = PREFIXES[getQuickAddMagicMode()]
|
||||
if (prefixes === undefined) {
|
||||
return null
|
||||
}
|
||||
listPrefix = prefixes.label
|
||||
projectPrefix = prefixes.label
|
||||
}
|
||||
return getItemsFromPrefix(text, listPrefix)
|
||||
return getItemsFromPrefix(text, projectPrefix)
|
||||
}
|
||||
|
||||
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 => {
|
||||
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
|
||||
// Not removing assignees to avoid removing @text where the user does not exist
|
||||
result.text = result.text.trim()
|
||||
|
@ -1,5 +1,5 @@
|
||||
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', () => {
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
@ -15,68 +15,68 @@ test('return a saved history', () => {
|
||||
expect(h).toStrictEqual(saved)
|
||||
})
|
||||
|
||||
test('store list in history', () => {
|
||||
test('store project in history', () => {
|
||||
let saved = {}
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
Storage.prototype.setItem = vi.fn((key, lists) => {
|
||||
saved = lists
|
||||
Storage.prototype.setItem = vi.fn((key, projects) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
saveListToHistory({id: 1})
|
||||
saveProjectToHistory({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
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, lists: string) => {
|
||||
saved = lists
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
saveListToHistory({id: 1})
|
||||
saveListToHistory({id: 2})
|
||||
saveListToHistory({id: 3})
|
||||
saveListToHistory({id: 4})
|
||||
saveListToHistory({id: 5})
|
||||
saveListToHistory({id: 6})
|
||||
saveProjectToHistory({id: 1})
|
||||
saveProjectToHistory({id: 2})
|
||||
saveProjectToHistory({id: 3})
|
||||
saveProjectToHistory({id: 4})
|
||||
saveProjectToHistory({id: 5})
|
||||
saveProjectToHistory({id: 6})
|
||||
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
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, lists: string) => {
|
||||
saved = lists
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
saveListToHistory({id: 1})
|
||||
saveListToHistory({id: 1})
|
||||
saveProjectToHistory({id: 1})
|
||||
saveProjectToHistory({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
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, lists: string) => {
|
||||
saved = lists
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
saveListToHistory({id: 1})
|
||||
saveListToHistory({id: 2})
|
||||
saveListToHistory({id: 1})
|
||||
saveProjectToHistory({id: 1})
|
||||
saveProjectToHistory({id: 2})
|
||||
saveProjectToHistory({id: 1})
|
||||
expect(saved).toBe('[{"id":1},{"id":2}]')
|
||||
})
|
||||
|
||||
test('remove list from history', () => {
|
||||
test('remove project from history', () => {
|
||||
let saved: string | null = '[{"id": 1}]'
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
Storage.prototype.setItem = vi.fn((key: string, lists: string) => {
|
||||
saved = lists
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
Storage.prototype.removeItem = vi.fn((key: string) => {
|
||||
saved = null
|
||||
})
|
||||
|
||||
removeListFromHistory({id: 1})
|
||||
removeProjectFromHistory({id: 1})
|
||||
expect(saved).toBeNull()
|
||||
})
|
||||
|
@ -1,9 +1,9 @@
|
||||
export interface ListHistory {
|
||||
export interface ProjectHistory {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export function getHistory(): ListHistory[] {
|
||||
const savedHistory = localStorage.getItem('listHistory')
|
||||
export function getHistory(): ProjectHistory[] {
|
||||
const savedHistory = localStorage.getItem('projectHistory')
|
||||
if (savedHistory === null) {
|
||||
return []
|
||||
}
|
||||
@ -11,27 +11,27 @@ export function getHistory(): ListHistory[] {
|
||||
return JSON.parse(savedHistory)
|
||||
}
|
||||
|
||||
function saveHistory(history: ListHistory[]) {
|
||||
function saveHistory(history: ProjectHistory[]) {
|
||||
if (history.length === 0) {
|
||||
localStorage.removeItem('listHistory')
|
||||
localStorage.removeItem('projectHistory')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('listHistory', JSON.stringify(history))
|
||||
localStorage.setItem('projectHistory', JSON.stringify(history))
|
||||
}
|
||||
|
||||
export function saveListToHistory(list: ListHistory) {
|
||||
const history: ListHistory[] = getHistory()
|
||||
export function saveProjectToHistory(project: ProjectHistory) {
|
||||
const history: ProjectHistory[] = getHistory()
|
||||
|
||||
// Remove the element if it already exists in history, preventing duplicates and essentially moving it to the beginning
|
||||
history.forEach((l, i) => {
|
||||
if (l.id === list.id) {
|
||||
if (l.id === project.id) {
|
||||
history.splice(i, 1)
|
||||
}
|
||||
})
|
||||
|
||||
// Add the new list to the beginning of the list
|
||||
history.unshift(list)
|
||||
// Add the new project to the beginning of the project
|
||||
history.unshift(project)
|
||||
|
||||
if (history.length > 5) {
|
||||
history.pop()
|
||||
@ -39,11 +39,11 @@ export function saveListToHistory(list: ListHistory) {
|
||||
saveHistory(history)
|
||||
}
|
||||
|
||||
export function removeListFromHistory(list: ListHistory) {
|
||||
const history: ListHistory[] = getHistory()
|
||||
export function removeProjectFromHistory(project: ProjectHistory) {
|
||||
const history: ProjectHistory[] = getHistory()
|
||||
|
||||
history.forEach((l, i) => {
|
||||
if (l.id === list.id) {
|
||||
if (l.id === project.id) {
|
||||
history.splice(i, 1)
|
||||
}
|
||||
})
|
||||
|
@ -2,12 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteLocation } from 'vue-router'
|
||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
|
||||
import {saveListView, getListView} from '@/helpers/saveListView'
|
||||
import {saveProjectView, getProjectView} from '@/helpers/saveProjectView'
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
|
||||
import {setTitle} from '@/helpers/setTitle'
|
||||
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
@ -33,20 +33,20 @@ const NewLabelComponent = () => import('@/views/labels/NewLabel.vue')
|
||||
// Migration
|
||||
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
|
||||
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
|
||||
// List Views
|
||||
const ListList = () => import('@/views/list/ListList.vue')
|
||||
const ListGantt = () => import('@/views/list/ListGantt.vue')
|
||||
const ListTable = () => import('@/views/list/ListTable.vue')
|
||||
const ListKanban = () => import('@/views/list/ListKanban.vue')
|
||||
const ListInfo = () => import('@/views/list/ListInfo.vue')
|
||||
// Project Views
|
||||
const ProjectList = () => import('@/views/project/ProjectList.vue')
|
||||
const ProjectGantt = () => import('@/views/project/ProjectGantt.vue')
|
||||
const ProjectTable = () => import('@/views/project/ProjectTable.vue')
|
||||
const ProjectKanban = () => import('@/views/project/ProjectKanban.vue')
|
||||
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
|
||||
|
||||
// List Settings
|
||||
const ListSettingEdit = () => import('@/views/list/settings/edit.vue')
|
||||
const ListSettingBackground = () => import('@/views/list/settings/background.vue')
|
||||
const ListSettingDuplicate = () => import('@/views/list/settings/duplicate.vue')
|
||||
const ListSettingShare = () => import('@/views/list/settings/share.vue')
|
||||
const ListSettingDelete = () => import('@/views/list/settings/delete.vue')
|
||||
const ListSettingArchive = () => import('@/views/list/settings/archive.vue')
|
||||
// Project Settings
|
||||
const ProjectSettingEdit = () => import('@/views/project/settings/edit.vue')
|
||||
const ProjectSettingBackground = () => import('@/views/project/settings/background.vue')
|
||||
const ProjectSettingDuplicate = () => import('@/views/project/settings/duplicate.vue')
|
||||
const ProjectSettingShare = () => import('@/views/project/settings/share.vue')
|
||||
const ProjectSettingDelete = () => import('@/views/project/settings/delete.vue')
|
||||
const ProjectSettingArchive = () => import('@/views/project/settings/archive.vue')
|
||||
|
||||
// Namespace Settings
|
||||
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 UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
|
||||
|
||||
// List Handling
|
||||
const NewListComponent = () => import('@/views/list/NewList.vue')
|
||||
// Project Handling
|
||||
const NewProjectComponent = () => import('@/views/project/NewProject.vue')
|
||||
|
||||
// Namespace Handling
|
||||
const NewNamespaceComponent = () => import('@/views/namespaces/NewNamespace.vue')
|
||||
@ -269,142 +269,142 @@ const router = createRouter({
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/lists/new/:namespaceId/',
|
||||
name: 'list.create',
|
||||
component: NewListComponent,
|
||||
path: '/projects/new/:namespaceId/',
|
||||
name: 'project.create',
|
||||
component: NewProjectComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'list.settings.edit',
|
||||
component: ListSettingEdit,
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
path: '/projects/:projectId/settings/edit',
|
||||
name: 'project.settings.edit',
|
||||
component: ProjectSettingEdit,
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/background',
|
||||
name: 'list.settings.background',
|
||||
component: ListSettingBackground,
|
||||
path: '/projects/:projectId/settings/background',
|
||||
name: 'project.settings.background',
|
||||
component: ProjectSettingBackground,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/duplicate',
|
||||
name: 'list.settings.duplicate',
|
||||
component: ListSettingDuplicate,
|
||||
path: '/projects/:projectId/settings/duplicate',
|
||||
name: 'project.settings.duplicate',
|
||||
component: ProjectSettingDuplicate,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/share',
|
||||
name: 'list.settings.share',
|
||||
component: ListSettingShare,
|
||||
path: '/projects/:projectId/settings/share',
|
||||
name: 'project.settings.share',
|
||||
component: ProjectSettingShare,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'list.settings.delete',
|
||||
component: ListSettingDelete,
|
||||
path: '/projects/:projectId/settings/delete',
|
||||
name: 'project.settings.delete',
|
||||
component: ProjectSettingDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/archive',
|
||||
name: 'list.settings.archive',
|
||||
component: ListSettingArchive,
|
||||
path: '/projects/:projectId/settings/archive',
|
||||
name: 'project.settings.archive',
|
||||
component: ProjectSettingArchive,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
path: '/projects/:projectId/settings/edit',
|
||||
name: 'filter.settings.edit',
|
||||
component: FilterEdit,
|
||||
meta: {
|
||||
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',
|
||||
component: FilterDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/info',
|
||||
name: 'list.info',
|
||||
component: ListInfo,
|
||||
path: '/projects/:projectId/info',
|
||||
name: 'project.info',
|
||||
component: ProjectInfo,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId',
|
||||
name: 'list.index',
|
||||
path: '/projects/:projectId',
|
||||
name: 'project.index',
|
||||
redirect(to) {
|
||||
// Redirect the user to list view by default
|
||||
|
||||
const savedListView = getListView(to.params.listId)
|
||||
console.debug('Replaced list view with', savedListView)
|
||||
const savedProjectView = getProjectView(to.params.projectId)
|
||||
console.debug('Replaced list view with', savedProjectView)
|
||||
|
||||
return {
|
||||
name: router.hasRoute(savedListView)
|
||||
? savedListView
|
||||
: 'list.list',
|
||||
params: {listId: to.params.listId},
|
||||
name: router.hasRoute(savedProjectView)
|
||||
? savedProjectView
|
||||
: 'project.list',
|
||||
params: {projectId: to.params.projectId},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/list',
|
||||
name: 'list.list',
|
||||
component: ListList,
|
||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
path: '/projects/:projectId/list',
|
||||
name: 'project.list',
|
||||
component: ProjectList,
|
||||
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/gantt',
|
||||
name: 'list.gantt',
|
||||
component: ListGantt,
|
||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
||||
path: '/projects/:projectId/gantt',
|
||||
name: 'project.gantt',
|
||||
component: ProjectGantt,
|
||||
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
|
||||
// FIXME: test if `useRoute` would be the same. If it would use it instead.
|
||||
props: route => ({route}),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/table',
|
||||
name: 'list.table',
|
||||
component: ListTable,
|
||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
path: '/projects/:projectId/table',
|
||||
name: 'project.table',
|
||||
component: ProjectTable,
|
||||
beforeEnter: (to) => saveProjectView(to.params.projectId, to.name),
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/kanban',
|
||||
name: 'list.kanban',
|
||||
component: ListKanban,
|
||||
path: '/projects/:projectId/kanban',
|
||||
name: 'project.kanban',
|
||||
component: ProjectKanban,
|
||||
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
|
||||
const listStore = useListStore()
|
||||
const listFromStore = listStore.getListById(Number(to.params.listId))
|
||||
if(listFromStore) {
|
||||
setTitle(listFromStore.title)
|
||||
const projectStore = useProjectStore()
|
||||
const projectFromStore = projectStore.getProjectById(Number(to.params.projectId))
|
||||
if(projectFromStore) {
|
||||
setTitle(projectFromStore.title)
|
||||
}
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
props: route => ({ projectId: Number(route.params.projectId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/teams',
|
||||
|
@ -1,13 +1,13 @@
|
||||
import AbstractService from './abstractService'
|
||||
import BackgroundImageModel from '../models/backgroundImage'
|
||||
import ListModel from '@/models/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
import type { IBackgroundImage } from '@/modelTypes/IBackgroundImage'
|
||||
|
||||
export default class BackgroundUnsplashService extends AbstractService<IBackgroundImage> {
|
||||
constructor() {
|
||||
super({
|
||||
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) {
|
||||
return new ListModel(data)
|
||||
return new ProjectModel(data)
|
||||
}
|
||||
|
||||
async thumb(model) {
|
||||
|
@ -1,13 +1,13 @@
|
||||
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'
|
||||
|
||||
export default class BackgroundUploadService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listId}/backgrounds/upload',
|
||||
create: '/projects/{projectId}/backgrounds/upload',
|
||||
})
|
||||
}
|
||||
|
||||
@ -15,16 +15,16 @@ export default class BackgroundUploadService extends AbstractService {
|
||||
return false
|
||||
}
|
||||
|
||||
modelCreateFactory(data: Partial<IList>) {
|
||||
return new ListModel(data)
|
||||
modelCreateFactory(data: Partial<IProject>) {
|
||||
return new ProjectModel(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to the server
|
||||
*/
|
||||
create(listId: IList['id'], file: IFile) {
|
||||
create(projectId: IProject['id'], file: IFile) {
|
||||
return this.uploadFile(
|
||||
this.getReplacedRoute(this.paths.create, {listId}),
|
||||
this.getReplacedRoute(this.paths.create, {projectId}),
|
||||
file,
|
||||
'background',
|
||||
)
|
||||
|
@ -6,10 +6,10 @@ import type { IBucket } from '@/modelTypes/IBucket'
|
||||
export default class BucketService extends AbstractService<IBucket> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/lists/{listId}/buckets',
|
||||
create: '/lists/{listId}/buckets',
|
||||
update: '/lists/{listId}/buckets/{id}',
|
||||
delete: '/lists/{listId}/buckets/{id}',
|
||||
getAll: '/projects/{projectId}/buckets',
|
||||
create: '/projects/{projectId}/buckets',
|
||||
update: '/projects/{projectId}/buckets/{id}',
|
||||
delete: '/projects/{projectId}/buckets/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -5,10 +5,10 @@ import type {ILinkShare} from '@/modelTypes/ILinkShare'
|
||||
export default class LinkShareService extends AbstractService<ILinkShare> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/lists/{listId}/shares',
|
||||
get: '/lists/{listId}/shares/{id}',
|
||||
create: '/lists/{listId}/shares',
|
||||
delete: '/lists/{listId}/shares/{id}',
|
||||
getAll: '/projects/{projectId}/shares',
|
||||
get: '/projects/{projectId}/shares/{id}',
|
||||
create: '/projects/{projectId}/shares',
|
||||
delete: '/projects/{projectId}/shares/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,22 +1,22 @@
|
||||
import AbstractService from './abstractService'
|
||||
import ListModel from '@/models/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import ProjectModel from '@/models/project'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import TaskService from './task'
|
||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||
|
||||
export default class ListService extends AbstractService<IList> {
|
||||
export default class ProjectService extends AbstractService<IProject> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/namespaces/{namespaceId}/lists',
|
||||
get: '/lists/{id}',
|
||||
getAll: '/lists',
|
||||
update: '/lists/{id}',
|
||||
delete: '/lists/{id}',
|
||||
create: '/namespaces/{namespaceId}/projects',
|
||||
get: '/projects/{id}',
|
||||
getAll: '/projects',
|
||||
update: '/projects/{id}',
|
||||
delete: '/projects/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new ListModel(data)
|
||||
return new ProjectModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
@ -34,29 +34,29 @@ export default class ListService extends AbstractService<IList> {
|
||||
return model
|
||||
}
|
||||
|
||||
beforeCreate(list) {
|
||||
list.hexColor = colorFromHex(list.hexColor)
|
||||
return list
|
||||
beforeCreate(project) {
|
||||
project.hexColor = colorFromHex(project.hexColor)
|
||||
return project
|
||||
}
|
||||
|
||||
async background(list: Pick<IList, 'id' | 'backgroundInformation'>) {
|
||||
if (list.backgroundInformation === null) {
|
||||
async background(project: Pick<IProject, 'id' | 'backgroundInformation'>) {
|
||||
if (project.backgroundInformation === null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const response = await this.http({
|
||||
url: `/lists/${list.id}/background`,
|
||||
url: `/projects/${project.id}/background`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
|
||||
async removeBackground(list: Pick<IList, 'id'>) {
|
||||
async removeBackground(project: Pick<IProject, 'id'>) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
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
|
||||
} finally {
|
||||
cancel()
|
@ -1,21 +1,21 @@
|
||||
import AbstractService from './abstractService'
|
||||
import listDuplicateModel from '@/models/listDuplicateModel'
|
||||
import type {IListDuplicate} from '@/modelTypes/IListDuplicate'
|
||||
import projectDuplicateModel from '@/models/projectDuplicateModel'
|
||||
import type {IProjectDuplicate} from '@/modelTypes/IProjectDuplicate'
|
||||
|
||||
export default class ListDuplicateService extends AbstractService<IListDuplicate> {
|
||||
export default class ProjectDuplicateService extends AbstractService<IProjectDuplicate> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listId}/duplicate',
|
||||
create: '/projects/{projectId}/duplicate',
|
||||
})
|
||||
}
|
||||
|
||||
beforeCreate(model) {
|
||||
|
||||
model.list = null
|
||||
model.project = null
|
||||
return model
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new listDuplicateModel(data)
|
||||
return new projectDuplicateModel(data)
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import AbstractService from './abstractService'
|
||||
import UserModel from '../models/user'
|
||||
|
||||
export default class ListUserService extends AbstractService {
|
||||
export default class ProjectUserService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/lists/{listId}/listusers',
|
||||
getAll: '/projects/{projectId}/projectusers',
|
||||
})
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import {useI18n} from 'vue-i18n'
|
||||
import type {MaybeRef} 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 AbstractService from '@/services/abstractService'
|
||||
@ -16,31 +16,31 @@ import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
import {objectToSnakeCase, objectToCamelCase} from '@/helpers/case'
|
||||
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.
|
||||
*/
|
||||
function getListId(savedFilter: ISavedFilter) {
|
||||
let listId = savedFilter.id * -1 - 1
|
||||
if (listId > 0) {
|
||||
listId = 0
|
||||
function getProjectId(savedFilter: ISavedFilter) {
|
||||
let projectId = savedFilter.id * -1 - 1
|
||||
if (projectId > 0) {
|
||||
projectId = 0
|
||||
}
|
||||
return listId
|
||||
return projectId
|
||||
}
|
||||
|
||||
export function getSavedFilterIdFromListId(listId: IList['id']) {
|
||||
let filterId = listId * -1 - 1
|
||||
// FilterIds from listIds are always positive
|
||||
export function getSavedFilterIdFromProjectId(projectId: IProject['id']) {
|
||||
let filterId = projectId * -1 - 1
|
||||
// FilterIds from projectIds are always positive
|
||||
if (filterId < 0) {
|
||||
filterId = 0
|
||||
}
|
||||
return filterId
|
||||
}
|
||||
|
||||
export function isSavedFilter(list: IList) {
|
||||
return getSavedFilterIdFromListId(list.id) > 0
|
||||
export function isSavedFilter(project: IProject) {
|
||||
return getSavedFilterIdFromProjectId(project.id) > 0
|
||||
}
|
||||
|
||||
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 {t} = useI18n({useScope:'global'})
|
||||
const namespaceStore = useNamespaceStore()
|
||||
@ -94,13 +94,13 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
|
||||
})
|
||||
|
||||
// load SavedFilter
|
||||
watch(() => unref(listId), async (watchedListId) => {
|
||||
if (watchedListId === undefined) {
|
||||
watch(() => unref(projectId), async (watchedProjectId) => {
|
||||
if (watchedProjectId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// We assume the listId in the route is the pseudolist
|
||||
const savedFilterId = getSavedFilterIdFromListId(watchedListId)
|
||||
// We assume the projectId in the route is the pseudoproject
|
||||
const savedFilterId = getSavedFilterIdFromProjectId(watchedProjectId)
|
||||
|
||||
filter.value = new SavedFilterModel({id: savedFilterId})
|
||||
const response = await filterService.get(filter.value)
|
||||
@ -111,7 +111,7 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
|
||||
async function createFilter() {
|
||||
filter.value = await filterService.create(filter.value)
|
||||
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() {
|
||||
@ -120,8 +120,8 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
|
||||
success({message: t('filters.edit.success')})
|
||||
response.filters = objectToSnakeCase(response.filters)
|
||||
filter.value = response
|
||||
await useBaseStore().setCurrentList(new ListModel({
|
||||
id: getListId(filter.value),
|
||||
await useBaseStore().setCurrentProject(new ProjectModel({
|
||||
id: getProjectId(filter.value),
|
||||
title: filter.value.title,
|
||||
}))
|
||||
router.back()
|
||||
|
@ -18,7 +18,7 @@ const parseDate = date => {
|
||||
export default class TaskService extends AbstractService<ITask> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listId}',
|
||||
create: '/projects/{projectId}',
|
||||
getAll: '/tasks/all',
|
||||
get: '/tasks/{id}',
|
||||
update: '/tasks/{id}',
|
||||
@ -43,8 +43,8 @@ export default class TaskService extends AbstractService<ITask> {
|
||||
|
||||
model.title = model.title?.trim()
|
||||
|
||||
// Ensure that listId is an int
|
||||
model.listId = Number(model.listId)
|
||||
// Ensure that projectId is an int
|
||||
model.projectId = Number(model.projectId)
|
||||
|
||||
// Convert dates into an iso string
|
||||
model.dueDate = parseDate(model.dueDate)
|
||||
|
@ -17,7 +17,7 @@ export interface GetAllTasksParams {
|
||||
export default class TaskCollectionService extends AbstractService<ITask> {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/lists/{listId}/tasks',
|
||||
getAll: '/projects/{projectId}/tasks',
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
import AbstractService from './abstractService'
|
||||
import TeamListModel from '@/models/teamList'
|
||||
import type {ITeamList} from '@/modelTypes/ITeamList'
|
||||
import TeamProjectModel from '@/models/teamProject'
|
||||
import type {ITeamProject} from '@/modelTypes/ITeamProject'
|
||||
import TeamModel from '@/models/team'
|
||||
|
||||
export default class TeamListService extends AbstractService<ITeamList> {
|
||||
export default class TeamProjectService extends AbstractService<ITeamProject> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listId}/teams',
|
||||
getAll: '/lists/{listId}/teams',
|
||||
update: '/lists/{listId}/teams/{teamId}',
|
||||
delete: '/lists/{listId}/teams/{teamId}',
|
||||
create: '/projects/{projectId}/teams',
|
||||
getAll: '/projects/{projectId}/teams',
|
||||
update: '/projects/{projectId}/teams/{teamId}',
|
||||
delete: '/projects/{projectId}/teams/{teamId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new TeamListModel(data)
|
||||
return new TeamProjectModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
|
@ -1,20 +1,20 @@
|
||||
import AbstractService from './abstractService'
|
||||
import UserListModel from '@/models/userList'
|
||||
import type {IUserList} from '@/modelTypes/IUserList'
|
||||
import UserProjectModel from '@/models/userProject'
|
||||
import type {IUserProject} from '@/modelTypes/IUserProject'
|
||||
import UserModel from '@/models/user'
|
||||
|
||||
export default class UserListService extends AbstractService<IUserList> {
|
||||
export default class UserProjectService extends AbstractService<IUserProject> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listId}/users',
|
||||
getAll: '/lists/{listId}/users',
|
||||
update: '/lists/{listId}/users/{userId}',
|
||||
delete: '/lists/{listId}/users/{userId}',
|
||||
create: '/projects/{projectId}/users',
|
||||
getAll: '/projects/{projectId}/users',
|
||||
update: '/projects/{projectId}/users/{userId}',
|
||||
delete: '/projects/{projectId}/users/{userId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new UserListModel(data)
|
||||
return new UserProjectModel(data)
|
||||
}
|
||||
|
||||
modelGetAllFactory(data) {
|
||||
|
@ -3,21 +3,21 @@ import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
import ListService from '../services/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
import ProjectService from '../services/project'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
import {useMenuActive} from '@/composables/useMenuActive'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
export const useBaseStore = defineStore('base', () => {
|
||||
const loading = ref(false)
|
||||
const ready = ref(false)
|
||||
|
||||
// This is used to highlight the current list in menu for all list related views
|
||||
const currentList = ref<IList | null>(new ListModel({
|
||||
// This is used to highlight the current project in menu for all project related views
|
||||
const currentProject = ref<IProject | null>(new ProjectModel({
|
||||
id: 0,
|
||||
isArchived: false,
|
||||
}))
|
||||
@ -33,21 +33,21 @@ export const useBaseStore = defineStore('base', () => {
|
||||
loading.value = newLoading
|
||||
}
|
||||
|
||||
function setCurrentList(newCurrentList: IList | null) {
|
||||
// Server updates don't return the right. Therefore, the right is reset after updating the list which is
|
||||
function setCurrentProject(newCurrentProject: IProject | null) {
|
||||
// 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
|
||||
// when updating the list in global state.
|
||||
// when updating the project in global state.
|
||||
if (
|
||||
typeof currentList.value?.maxRight !== 'undefined' &&
|
||||
newCurrentList !== null &&
|
||||
typeof currentProject.value?.maxRight !== 'undefined' &&
|
||||
newCurrentProject !== null &&
|
||||
(
|
||||
typeof newCurrentList.maxRight === 'undefined' ||
|
||||
newCurrentList.maxRight === null
|
||||
typeof newCurrentProject.maxRight === 'undefined' ||
|
||||
newCurrentProject.maxRight === null
|
||||
)
|
||||
) {
|
||||
newCurrentList.maxRight = currentList.value.maxRight
|
||||
newCurrentProject.maxRight = currentProject.value.maxRight
|
||||
}
|
||||
currentList.value = newCurrentList
|
||||
currentProject.value = newCurrentProject
|
||||
}
|
||||
|
||||
function setHasTasks(newHasTasks: boolean) {
|
||||
@ -78,44 +78,44 @@ export const useBaseStore = defineStore('base', () => {
|
||||
ready.value = value
|
||||
}
|
||||
|
||||
async function handleSetCurrentList(
|
||||
{list, forceUpdate = false}: {list: IList | null, forceUpdate?: boolean},
|
||||
async function handleSetCurrentProject(
|
||||
{project, forceUpdate = false}: {project: IProject | null, forceUpdate?: boolean},
|
||||
) {
|
||||
if (list === null) {
|
||||
setCurrentList({})
|
||||
if (project === null) {
|
||||
setCurrentProject({})
|
||||
setBackground('')
|
||||
setBlurHash('')
|
||||
return
|
||||
}
|
||||
|
||||
// The forceUpdate parameter is used only when updating a list background directly because in that case
|
||||
// the current list stays the same, but we want to show the new background right away.
|
||||
if (list.id !== currentList.value?.id || forceUpdate) {
|
||||
if (list.backgroundInformation) {
|
||||
// The forceUpdate parameter is used only when updating a project background directly because in that case
|
||||
// the current project stays the same, but we want to show the new background right away.
|
||||
if (project.id !== currentProject.value?.id || forceUpdate) {
|
||||
if (project.backgroundInformation) {
|
||||
try {
|
||||
const blurHash = await getBlobFromBlurHash(list.backgroundBlurHash)
|
||||
const blurHash = await getBlobFromBlurHash(project.backgroundBlurHash)
|
||||
if (blurHash) {
|
||||
setBlurHash(window.URL.createObjectURL(blurHash))
|
||||
}
|
||||
|
||||
const listService = new ListService()
|
||||
const background = await listService.background(list)
|
||||
const projectService = new ProjectService()
|
||||
const background = await projectService.background(project)
|
||||
setBackground(background)
|
||||
} 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 (
|
||||
typeof list.backgroundInformation === 'undefined' ||
|
||||
list.backgroundInformation === null
|
||||
typeof project.backgroundInformation === 'undefined' ||
|
||||
project.backgroundInformation === null
|
||||
) {
|
||||
setBackground('')
|
||||
setBlurHash('')
|
||||
}
|
||||
|
||||
setCurrentList(list)
|
||||
setCurrentProject(project)
|
||||
}
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@ -128,7 +128,7 @@ export const useBaseStore = defineStore('base', () => {
|
||||
return {
|
||||
loading: readonly(loading),
|
||||
ready: readonly(ready),
|
||||
currentList: readonly(currentList),
|
||||
currentProject: readonly(currentProject),
|
||||
background: readonly(background),
|
||||
blurHash: readonly(blurHash),
|
||||
hasTasks: readonly(hasTasks),
|
||||
@ -138,7 +138,7 @@ export const useBaseStore = defineStore('base', () => {
|
||||
|
||||
setLoading,
|
||||
setReady,
|
||||
setCurrentList,
|
||||
setCurrentProject,
|
||||
setHasTasks,
|
||||
setKeyboardShortcutsActive,
|
||||
setQuickActionsActive,
|
||||
@ -146,7 +146,7 @@ export const useBaseStore = defineStore('base', () => {
|
||||
setBlurHash,
|
||||
setLogoVisible,
|
||||
|
||||
handleSetCurrentList,
|
||||
handleSetCurrentProject,
|
||||
loadApp,
|
||||
|
||||
...useMenuActive(),
|
||||
|
@ -12,7 +12,7 @@ import TaskCollectionService from '@/services/taskCollection'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {IBucket} from '@/modelTypes/IBucket'
|
||||
|
||||
const TASKS_PER_BUCKET = 25
|
||||
@ -45,7 +45,7 @@ const addTaskToBucketAndSort = (buckets: IBucket[], task: ITask) => {
|
||||
*/
|
||||
export const useKanbanStore = defineStore('kanban', () => {
|
||||
const buckets = ref<IBucket[]>([])
|
||||
const listId = ref<IList['id']>(0)
|
||||
const projectId = ref<IProject['id']>(0)
|
||||
const bucketLoading = ref<{[id: IBucket['id']]: boolean}>({})
|
||||
const taskPagesPerBucket = ref<{[id: IBucket['id']]: number}>({})
|
||||
const allTasksLoadedForBucket = ref<{[id: IBucket['id']]: boolean}>({})
|
||||
@ -68,8 +68,8 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
isLoading.value = newIsLoading
|
||||
}
|
||||
|
||||
function setListId(newListId: IList['id']) {
|
||||
listId.value = Number(newListId)
|
||||
function setProjectId(newProjectId: IProject['id']) {
|
||||
projectId.value = Number(newProjectId)
|
||||
}
|
||||
|
||||
function setBuckets(newBuckets: IBucket[]) {
|
||||
@ -223,20 +223,20 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
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)
|
||||
|
||||
// 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([])
|
||||
|
||||
const bucketService = new BucketService()
|
||||
try {
|
||||
const newBuckets = await bucketService.getAll({listId}, {
|
||||
const newBuckets = await bucketService.getAll({projectId}, {
|
||||
...params,
|
||||
per_page: TASKS_PER_BUCKET,
|
||||
})
|
||||
setBuckets(newBuckets)
|
||||
setListId(listId)
|
||||
setProjectId(projectId)
|
||||
return newBuckets
|
||||
} finally {
|
||||
cancel()
|
||||
@ -244,8 +244,8 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
}
|
||||
|
||||
async function loadNextTasksForBucket(
|
||||
{listId, ps = {}, bucketId} :
|
||||
{listId: IList['id'], ps, bucketId: IBucket['id']},
|
||||
{projectId, ps = {}, bucketId} :
|
||||
{projectId: IProject['id'], ps, bucketId: IBucket['id']},
|
||||
) {
|
||||
const isLoading = bucketLoading.value[bucketId] ?? false
|
||||
if (isLoading) {
|
||||
@ -288,7 +288,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
|
||||
const taskService = new TaskCollectionService()
|
||||
try {
|
||||
const tasks = await taskService.getAll({listId}, params, page)
|
||||
const tasks = await taskService.getAll({projectId}, params, page)
|
||||
addTasksToBucket({tasks, bucketId: bucketId})
|
||||
setTasksLoadedForBucketPage({bucketId, page})
|
||||
if (taskService.totalPages <= page) {
|
||||
@ -322,7 +322,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
const response = await bucketService.delete(bucket)
|
||||
removeBucket(bucket)
|
||||
// We reload all buckets because tasks are being moved from the deleted bucket
|
||||
loadBucketsForList({listId: bucket.listId, params})
|
||||
loadBucketsForProject({projectId: bucket.projectId, params})
|
||||
return response
|
||||
} finally {
|
||||
cancel()
|
||||
@ -366,7 +366,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
}
|
||||
|
||||
await updateBucket({ id, title })
|
||||
success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')})
|
||||
success({message: i18n.global.t('project.kanban.bucketTitleSavedSuccess')})
|
||||
}
|
||||
|
||||
return {
|
||||
@ -382,7 +382,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
||||
setTaskInBucket,
|
||||
addTaskToBucket,
|
||||
removeTaskInBucket,
|
||||
loadBucketsForList,
|
||||
loadBucketsForProject,
|
||||
loadNextTasksForBucket,
|
||||
createBucket,
|
||||
deleteBucket,
|
||||
|
@ -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(() => {
|
||||
return (labelsToHide: ILabel[], query: string) => {
|
||||
|
@ -5,29 +5,29 @@ import NamespaceService from '../services/namespace'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
|
||||
|
||||
export const useNamespaceStore = defineStore('namespace', () => {
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const isLoading = ref(false)
|
||||
// FIXME: should be object with id as key
|
||||
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) {
|
||||
|
||||
if (ignorePseudoNamespaces && namespaces.value[n].id < 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const l in namespaces.value[n].lists) {
|
||||
if (namespaces.value[n].lists[l].id === listId) {
|
||||
for (const l in namespaces.value[n].projects) {
|
||||
if (namespaces.value[n].projects[l].id === projectId) {
|
||||
return {
|
||||
list: namespaces.value[n].lists[l],
|
||||
project: namespaces.value[n].projects[l],
|
||||
namespace: namespaces.value[n],
|
||||
}
|
||||
}
|
||||
@ -60,9 +60,9 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
||||
newNamespaces.forEach(n => {
|
||||
add(n)
|
||||
|
||||
// Check for each list in that namespace if it has a subscription and set it if not
|
||||
n.lists.forEach(l => {
|
||||
if (l.subscription === null || l.subscription.entity !== 'list') {
|
||||
// Check for each project in that namespace if it has a subscription and set it if not
|
||||
n.projects.forEach(l => {
|
||||
if (l.subscription === null || l.subscription.entity !== 'project') {
|
||||
l.subscription = n.subscription
|
||||
}
|
||||
})
|
||||
@ -76,13 +76,13 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!namespace.lists || namespace.lists.length === 0) {
|
||||
namespace.lists = namespaces.value[namespaceIndex].lists
|
||||
if (!namespace.projects || namespace.projects.length === 0) {
|
||||
namespace.projects = namespaces.value[namespaceIndex].projects
|
||||
}
|
||||
|
||||
// Check for each list in that namespace if it has a subscription and set it if not
|
||||
namespace.lists.forEach(l => {
|
||||
if (l.subscription === null || l.subscription.entity !== 'list') {
|
||||
// Check for each project in that namespace if it has a subscription and set it if not
|
||||
namespace.projects.forEach(l => {
|
||||
if (l.subscription === null || l.subscription.entity !== 'project') {
|
||||
l.subscription = namespace.subscription
|
||||
}
|
||||
})
|
||||
@ -91,15 +91,15 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
||||
update(namespace)
|
||||
}
|
||||
|
||||
function setListInNamespaceById(list: IList) {
|
||||
function setProjectInNamespaceById(project: IProject) {
|
||||
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.
|
||||
if (namespaces.value[n].id === list.namespaceId) {
|
||||
for (const l in namespaces.value[n].lists) {
|
||||
if (namespaces.value[n].lists[l].id === list.id) {
|
||||
if (namespaces.value[n].id === project.namespaceId) {
|
||||
for (const l in namespaces.value[n].projects) {
|
||||
if (namespaces.value[n].projects[l].id === project.id) {
|
||||
const namespace = namespaces.value[n]
|
||||
namespace.lists[l] = list
|
||||
namespace.projects[l] = project
|
||||
namespaces.value[n] = namespace
|
||||
return
|
||||
}
|
||||
@ -123,23 +123,23 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function addListToNamespace(list: IList) {
|
||||
function addProjectToNamespace(project: IProject) {
|
||||
for (const n in namespaces.value) {
|
||||
if (namespaces.value[n].id === list.namespaceId) {
|
||||
namespaces.value[n].lists.push(list)
|
||||
if (namespaces.value[n].id === project.namespaceId) {
|
||||
namespaces.value[n].projects.push(project)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeListFromNamespaceById(list: IList) {
|
||||
function removeProjectFromNamespaceById(project: IProject) {
|
||||
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.
|
||||
if (namespaces.value[n].id === list.namespaceId) {
|
||||
for (const l in namespaces.value[n].lists) {
|
||||
if (namespaces.value[n].lists[l].id === list.id) {
|
||||
namespaces.value[n].lists.splice(l, 1)
|
||||
if (namespaces.value[n].id === project.namespaceId) {
|
||||
for (const l in namespaces.value[n].projects) {
|
||||
if (namespaces.value[n].projects[l].id === project.id) {
|
||||
namespaces.value[n].projects.splice(l, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -156,10 +156,10 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
||||
const namespaces = await namespaceService.getAll({}, {is_archived: true}) as INamespace[]
|
||||
setNamespaces(namespaces)
|
||||
|
||||
// Put all lists in the list state
|
||||
const lists = namespaces.flatMap(({lists}) => lists)
|
||||
// Put all projects in the project state
|
||||
const projects = namespaces.flatMap(({projects}) => projects)
|
||||
|
||||
listStore.setLists(lists)
|
||||
projectStore.setProjects(projects)
|
||||
|
||||
return namespaces
|
||||
} finally {
|
||||
@ -176,7 +176,7 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -211,17 +211,17 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
||||
isLoading: readonly(isLoading),
|
||||
namespaces: readonly(namespaces),
|
||||
|
||||
getListAndNamespaceById,
|
||||
getProjectAndNamespaceById,
|
||||
getNamespaceById,
|
||||
searchNamespace,
|
||||
|
||||
setNamespaces,
|
||||
setNamespaceById,
|
||||
setListInNamespaceById,
|
||||
setProjectInNamespaceById,
|
||||
addNamespace,
|
||||
removeNamespaceById,
|
||||
addListToNamespace,
|
||||
removeListFromNamespaceById,
|
||||
addProjectToNamespace,
|
||||
removeProjectFromNamespaceById,
|
||||
loadNamespaces,
|
||||
loadNamespacesIfFavoritesDontExist,
|
||||
removeFavoritesNamespaceIfEmpty,
|
||||
|
@ -2,57 +2,57 @@ import {watch, reactive, shallowReactive, unref, toRefs, readonly, ref, computed
|
||||
import {acceptHMRUpdate, defineStore} from 'pinia'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
import ProjectService from '@/services/project'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {removeListFromHistory} from '@/modules/listHistory'
|
||||
import {removeProjectFromHistory} from '@/modules/projectHistory'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import {useNamespaceStore} from './namespaces'
|
||||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import type {MaybeRef} from '@vueuse/core'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
import ProjectModel from '@/models/project'
|
||||
import {success} from '@/message'
|
||||
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 {
|
||||
[id: IList['id']]: IList
|
||||
export interface ProjectState {
|
||||
[id: IProject['id']]: IProject
|
||||
}
|
||||
|
||||
export const useListStore = defineStore('list', () => {
|
||||
export const useProjectStore = defineStore('project', () => {
|
||||
const baseStore = useBaseStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
// The lists are stored as an object which has the list ids as keys.
|
||||
const lists = ref<ListState>({})
|
||||
// The projects are stored as an object which has the project ids as keys.
|
||||
const projects = ref<ProjectState>({})
|
||||
|
||||
|
||||
const getListById = computed(() => {
|
||||
return (id: IList['id']) => typeof lists.value[id] !== 'undefined' ? lists.value[id] : null
|
||||
const getProjectById = computed(() => {
|
||||
return (id: IProject['id']) => typeof projects.value[id] !== 'undefined' ? projects.value[id] : null
|
||||
})
|
||||
|
||||
const findListByExactname = computed(() => {
|
||||
const findProjectByExactname = computed(() => {
|
||||
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 typeof list === 'undefined' ? null : list
|
||||
return typeof project === 'undefined' ? null : project
|
||||
}
|
||||
})
|
||||
|
||||
const searchList = computed(() => {
|
||||
const searchProject = computed(() => {
|
||||
return (query: string, includeArchived = false) => {
|
||||
return search(query)
|
||||
?.filter(value => value > 0)
|
||||
.map(id => lists.value[id])
|
||||
.filter(list => list.isArchived === includeArchived)
|
||||
.map(id => projects.value[id])
|
||||
.filter(project => project.isArchived === includeArchived)
|
||||
|| []
|
||||
}
|
||||
})
|
||||
@ -61,82 +61,82 @@ export const useListStore = defineStore('list', () => {
|
||||
isLoading.value = newIsLoading
|
||||
}
|
||||
|
||||
function setList(list: IList) {
|
||||
lists.value[list.id] = list
|
||||
update(list)
|
||||
function setProject(project: IProject) {
|
||||
projects.value[project.id] = project
|
||||
update(project)
|
||||
|
||||
if (baseStore.currentList?.id === list.id) {
|
||||
baseStore.setCurrentList(list)
|
||||
if (baseStore.currentProject?.id === project.id) {
|
||||
baseStore.setCurrentProject(project)
|
||||
}
|
||||
}
|
||||
|
||||
function setLists(newLists: IList[]) {
|
||||
newLists.forEach(l => {
|
||||
lists.value[l.id] = l
|
||||
function setProjects(newProjects: IProject[]) {
|
||||
newProjects.forEach(l => {
|
||||
projects.value[l.id] = l
|
||||
add(l)
|
||||
})
|
||||
}
|
||||
|
||||
function removeListById(list: IList) {
|
||||
remove(list)
|
||||
delete lists.value[list.id]
|
||||
function removeProjectById(project: IProject) {
|
||||
remove(project)
|
||||
delete projects.value[project.id]
|
||||
}
|
||||
|
||||
function toggleListFavorite(list: IList) {
|
||||
// The favorites pseudo list is always favorite
|
||||
// Archived lists cannot be marked favorite
|
||||
if (list.id === -1 || list.isArchived) {
|
||||
function toggleProjectFavorite(project: IProject) {
|
||||
// The favorites pseudo project is always favorite
|
||||
// Archived projects cannot be marked favorite
|
||||
if (project.id === -1 || project.isArchived) {
|
||||
return
|
||||
}
|
||||
return updateList({
|
||||
...list,
|
||||
isFavorite: !list.isFavorite,
|
||||
return updateProject({
|
||||
...project,
|
||||
isFavorite: !project.isFavorite,
|
||||
})
|
||||
}
|
||||
|
||||
async function createList(list: IList) {
|
||||
async function createProject(project: IProject) {
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const listService = new ListService()
|
||||
const projectService = new ProjectService()
|
||||
|
||||
try {
|
||||
const createdList = await listService.create(list)
|
||||
createdList.namespaceId = list.namespaceId
|
||||
namespaceStore.addListToNamespace(createdList)
|
||||
setList(createdList)
|
||||
return createdList
|
||||
const createdProject = await projectService.create(project)
|
||||
createdProject.namespaceId = project.namespaceId
|
||||
namespaceStore.addProjectToNamespace(createdProject)
|
||||
setProject(createdProject)
|
||||
return createdProject
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
async function updateList(list: IList) {
|
||||
async function updateProject(project: IProject) {
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
const listService = new ListService()
|
||||
const projectService = new ProjectService()
|
||||
|
||||
try {
|
||||
await listService.update(list)
|
||||
setList(list)
|
||||
namespaceStore.setListInNamespaceById(list)
|
||||
await projectService.update(project)
|
||||
setProject(project)
|
||||
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
|
||||
const newList = {
|
||||
...list,
|
||||
namespaceId: FavoriteListsNamespace,
|
||||
const newProject = {
|
||||
...project,
|
||||
namespaceId: FavoriteProjectsNamespace,
|
||||
}
|
||||
|
||||
namespaceStore.removeListFromNamespaceById(newList)
|
||||
if (list.isFavorite) {
|
||||
namespaceStore.addListToNamespace(newList)
|
||||
namespaceStore.removeProjectFromNamespaceById(newProject)
|
||||
if (project.isFavorite) {
|
||||
namespaceStore.addProjectToNamespace(newProject)
|
||||
}
|
||||
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||
namespaceStore.removeFavoritesNamespaceIfEmpty()
|
||||
return newList
|
||||
return newProject
|
||||
} catch (e) {
|
||||
// Reset the list state to the initial one to avoid confusion for the user
|
||||
setList({
|
||||
...list,
|
||||
isFavorite: !list.isFavorite,
|
||||
// Reset the project state to the initial one to avoid confusion for the user
|
||||
setProject({
|
||||
...project,
|
||||
isFavorite: !project.isFavorite,
|
||||
})
|
||||
throw e
|
||||
} 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 listService = new ListService()
|
||||
const projectService = new ProjectService()
|
||||
|
||||
try {
|
||||
const response = await listService.delete(list)
|
||||
removeListById(list)
|
||||
namespaceStore.removeListFromNamespaceById(list)
|
||||
removeListFromHistory({id: list.id})
|
||||
const response = await projectService.delete(project)
|
||||
removeProjectById(project)
|
||||
namespaceStore.removeProjectFromNamespaceById(project)
|
||||
removeProjectFromHistory({id: project.id})
|
||||
return response
|
||||
} finally {
|
||||
cancel()
|
||||
@ -161,51 +161,51 @@ export const useListStore = defineStore('list', () => {
|
||||
|
||||
return {
|
||||
isLoading: readonly(isLoading),
|
||||
lists: readonly(lists),
|
||||
projects: readonly(projects),
|
||||
|
||||
getListById,
|
||||
findListByExactname,
|
||||
searchList,
|
||||
getProjectById,
|
||||
findProjectByExactname,
|
||||
searchProject,
|
||||
|
||||
setList,
|
||||
setLists,
|
||||
removeListById,
|
||||
toggleListFavorite,
|
||||
createList,
|
||||
updateList,
|
||||
deleteList,
|
||||
setProject,
|
||||
setProjects,
|
||||
removeProjectById,
|
||||
toggleProjectFavorite,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
}
|
||||
})
|
||||
|
||||
export function useList(listId: MaybeRef<IList['id']>) {
|
||||
const listService = shallowReactive(new ListService())
|
||||
const {loading: isLoading} = toRefs(listService)
|
||||
const list: IList = reactive(new ListModel())
|
||||
export function useProject(projectId: MaybeRef<IProject['id']>) {
|
||||
const projectService = shallowReactive(new ProjectService())
|
||||
const {loading: isLoading} = toRefs(projectService)
|
||||
const project: IProject = reactive(new ProjectModel())
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
watch(
|
||||
() => unref(listId),
|
||||
async (listId) => {
|
||||
const loadedList = await listService.get(new ListModel({id: listId}))
|
||||
Object.assign(list, loadedList)
|
||||
() => unref(projectId),
|
||||
async (projectId) => {
|
||||
const loadedProject = await projectService.get(new ProjectModel({id: projectId}))
|
||||
Object.assign(project, loadedProject)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const listStore = useListStore()
|
||||
const projectStore = useProjectStore()
|
||||
async function save() {
|
||||
await listStore.updateList(list)
|
||||
success({message: t('list.edit.success')})
|
||||
await projectStore.updateProject(project)
|
||||
success({message: t('project.edit.success')})
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: readonly(isLoading),
|
||||
list,
|
||||
project,
|
||||
save,
|
||||
}
|
||||
}
|
||||
|
||||
// support hot reloading
|
||||
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
Loading…
x
Reference in New Issue
Block a user