Merge branch 'main' into fix/upcoming
# Conflicts: # src/views/tasks/ShowTasks.vue
This commit is contained in:
commit
d272eb2a7a
123 changed files with 3637 additions and 3309 deletions
28
.drone.yml
28
.drone.yml
|
@ -116,36 +116,16 @@ steps:
|
|||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
|
||||
CYPRESS_RECORD_KEY:
|
||||
from_secret: cypress_project_key
|
||||
commands:
|
||||
- sed -i 's/localhost/api/g' dist/index.html
|
||||
- yarn serve:dist & npx wait-on http://localhost:5000
|
||||
- yarn test:frontend --browser chrome
|
||||
- yarn test:frontend --browser chrome --record
|
||||
depends_on:
|
||||
- dependencies
|
||||
- build-prod
|
||||
|
||||
- name: upload-test-results
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: drone-test-results
|
||||
access_key:
|
||||
from_secret: test_results_aws_access_key_id
|
||||
secret_key:
|
||||
from_secret: test_results_aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
path_style: true
|
||||
source: cypress/screenshots/**/**/*
|
||||
strip_prefix: cypress/screenshots/
|
||||
target: /${DRONE_REPO}/${DRONE_PULL_REQUEST}_${DRONE_BRANCH}/${DRONE_BUILD_NUMBER}/
|
||||
depends_on:
|
||||
- test-frontend
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
- success
|
||||
|
||||
- name: deploy-preview
|
||||
image: node:16
|
||||
pull: true
|
||||
|
@ -665,6 +645,6 @@ steps:
|
|||
from_secret: crowdin_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de
|
||||
hmac: 997e1badebe484ac29557c4af356e63db4d3d57f3d32e92d482f117f8cec64da
|
||||
|
||||
...
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
"video": false,
|
||||
"retries": {
|
||||
"runMode": 2
|
||||
}
|
||||
},
|
||||
"projectId": "181c7x"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
|
||||
export class LinkShareFactory extends Factory {
|
||||
static table = 'link_shares'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
|
||||
export class ListFactory extends Factory {
|
||||
static table = 'lists'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import faker from 'faker'
|
||||
import faker from '@faker-js/faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from "date-fns"
|
||||
|
|
56
cypress/integration/list/list-history.spec.js
Normal file
56
cypress/integration/list/list-history.spec.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import {ListFactory} from '../../factories/list'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
describe('List History', () => {
|
||||
prepareLists()
|
||||
|
||||
it('should show a list history on the home page', () => {
|
||||
cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces')
|
||||
cy.intercept(Cypress.env('API_URL') + '/lists/*').as('loadList')
|
||||
|
||||
const lists = ListFactory.create(6)
|
||||
|
||||
cy.visit('/')
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.get('body')
|
||||
.should('not.contain', 'Last viewed')
|
||||
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[1].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[2].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[3].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[4].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
cy.visit(`/lists/${lists[5].id}`)
|
||||
cy.wait('@loadNamespaces')
|
||||
cy.wait('@loadList')
|
||||
|
||||
// cy.visit('/')
|
||||
// cy.wait('@loadNamespaces')
|
||||
// Not using cy.visit here to work around the redirect issue fixed in #1337
|
||||
cy.get('nav.menu.top-menu a')
|
||||
.contains('Overview')
|
||||
.click()
|
||||
|
||||
cy.get('body')
|
||||
.should('contain', 'Last viewed')
|
||||
cy.get('.list-cards-wrapper-2-rows')
|
||||
.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)
|
||||
})
|
||||
})
|
76
cypress/integration/list/list-view-gantt.spec.js
Normal file
76
cypress/integration/list/list-view-gantt.spec.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {formatISO, format} from 'date-fns'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('List View Gantt', () => {
|
||||
prepareLists()
|
||||
|
||||
it('Hides tasks with no dates', () => {
|
||||
const tasks = TaskFactory.create(1)
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.should('not.contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Shows tasks from the current and next month', () => {
|
||||
const now = new Date()
|
||||
const nextMonth = now
|
||||
nextMonth.setDate(1)
|
||||
nextMonth.setMonth(now.getMonth() + 1)
|
||||
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart .months')
|
||||
.should('contain', format(now, 'MMMM'))
|
||||
.should('contain', format(nextMonth, 'MMMM'))
|
||||
})
|
||||
|
||||
it('Shows tasks with dates', () => {
|
||||
const now = new Date()
|
||||
const tasks = TaskFactory.create(1, {
|
||||
start_date: formatISO(now),
|
||||
end_date: formatISO(now.setDate(now.getDate() + 4))
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.should('not.be.empty')
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Shows tasks with no dates after enabling them', () => {
|
||||
TaskFactory.create(1, {
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-options .fancycheckbox')
|
||||
.contains('Show tasks which don\'t have dates set')
|
||||
.click()
|
||||
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.should('not.be.empty')
|
||||
cy.get('.gantt-chart .tasks .task.nodate')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Drags a task around', () => {
|
||||
const now = new Date()
|
||||
TaskFactory.create(1, {
|
||||
start_date: formatISO(now),
|
||||
end_date: formatISO(now.setDate(now.getDate() + 4))
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart .tasks .task')
|
||||
.first()
|
||||
.trigger('mousedown', {which: 1})
|
||||
.trigger('mousemove', {clientX: 500, clientY: 0})
|
||||
.trigger('mouseup', {force: true})
|
||||
})
|
||||
})
|
196
cypress/integration/list/list-view-kanban.spec.js
Normal file
196
cypress/integration/list/list-view-kanban.spec.js
Normal file
|
@ -0,0 +1,196 @@
|
|||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('List View Kanban', () => {
|
||||
let buckets
|
||||
prepareLists()
|
||||
|
||||
beforeEach(() => {
|
||||
buckets = BucketFactory.create(2)
|
||||
})
|
||||
|
||||
it('Shows all buckets with their tasks', () => {
|
||||
const data = TaskFactory.create(10, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[0].title)
|
||||
.should('exist')
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[1].title)
|
||||
.should('exist')
|
||||
cy.get('.kanban .bucket')
|
||||
.first()
|
||||
.should('contain', data[0].title)
|
||||
})
|
||||
|
||||
it('Can add a new task to a bucket', () => {
|
||||
TaskFactory.create(2, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket')
|
||||
.contains(buckets[0].title)
|
||||
.get('.bucket-footer .button')
|
||||
.contains('Add another task')
|
||||
.click()
|
||||
cy.get('.kanban .bucket')
|
||||
.contains(buckets[0].title)
|
||||
.get('.bucket-footer .field .control input.input')
|
||||
.type('New Task{enter}')
|
||||
|
||||
cy.get('.kanban .bucket')
|
||||
.first()
|
||||
.should('contain', 'New Task')
|
||||
})
|
||||
|
||||
it('Can create a new bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket.new-bucket .button')
|
||||
.click()
|
||||
cy.get('.kanban .bucket.new-bucket input.input')
|
||||
.type('New Bucket{enter}')
|
||||
|
||||
cy.wait(1000) // Wait for the request to finish
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains('New Bucket')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can set a bucket limit', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||
.contains('Limit: Not Set')
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input')
|
||||
.first()
|
||||
.type(3)
|
||||
cy.get('[data-cy="setBucketLimit"]')
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header span.limit')
|
||||
.contains('0/3')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can rename a bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket .bucket-header .title')
|
||||
.first()
|
||||
.type('{selectall}New Bucket Title{enter}')
|
||||
cy.get('.kanban .bucket .bucket-header .title')
|
||||
.first()
|
||||
.should('contain', 'New Bucket Title')
|
||||
})
|
||||
|
||||
it('Can delete a bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||
.contains('Delete')
|
||||
.click()
|
||||
cy.get('.modal-mask .modal-container .modal-content .header')
|
||||
.should('contain', 'Delete the bucket')
|
||||
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
||||
.contains('Do it!')
|
||||
.click()
|
||||
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[0].title)
|
||||
.should('not.exist')
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[1].title)
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can drag tasks around', () => {
|
||||
const tasks = TaskFactory.create(2, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
.first()
|
||||
.drag('.kanban .bucket:nth-child(2) .tasks .dropper')
|
||||
|
||||
cy.get('.kanban .bucket:nth-child(2) .tasks')
|
||||
.should('contain', tasks[0].title)
|
||||
cy.get('.kanban .bucket:nth-child(1) .tasks')
|
||||
.should('not.contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Should navigate to the task when the task card is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.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)
|
||||
BucketFactory.create(2, {
|
||||
list_id: '{increment}',
|
||||
})
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
const task = tasks[0]
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket .tasks .task')
|
||||
.contains(task.title)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.get('.task-view .action-buttons .button', { timeout: 3000 })
|
||||
.contains('Move task')
|
||||
.click()
|
||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
||||
.type(`${lists[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')
|
||||
.children()
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification', { timeout: 1000 })
|
||||
.should('contain', 'Success')
|
||||
cy.go('back')
|
||||
cy.get('.kanban .bucket')
|
||||
.should('not.contain', task.title)
|
||||
})
|
||||
})
|
97
cypress/integration/list/list-view-list.spec.js
Normal file
97
cypress/integration/list/list-view-list.spec.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import {UserListFactory} from '../../factories/users_list'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('List View List', () => {
|
||||
prepareLists()
|
||||
|
||||
it('Should be an empty list', () => {
|
||||
cy.visit('/lists/1')
|
||||
cy.url()
|
||||
.should('contain', '/lists/1/list')
|
||||
cy.get('.list-title h1')
|
||||
.should('contain', 'First List')
|
||||
cy.get('.list-title .dropdown')
|
||||
.should('exist')
|
||||
cy.get('p')
|
||||
.contains('This list is currently empty.')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Should navigate to the task when the title is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/list')
|
||||
|
||||
cy.get('.tasks .task .tasktext')
|
||||
.contains(tasks[0].title)
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', `/tasks/${tasks[0].id}`)
|
||||
})
|
||||
|
||||
it('Should not see any elements for a list which is shared read only', () => {
|
||||
UserFactory.create(2)
|
||||
UserListFactory.create(1, {
|
||||
list_id: 2,
|
||||
user_id: 1,
|
||||
right: 0,
|
||||
})
|
||||
const lists = ListFactory.create(2, {
|
||||
owner_id: '{increment}',
|
||||
namespace_id: '{increment}',
|
||||
})
|
||||
cy.visit(`/lists/${lists[1].id}/`)
|
||||
|
||||
cy.get('.list-title a.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, {
|
||||
hex_color: '00db60',
|
||||
})
|
||||
TaskFactory.create(10, {
|
||||
list_id: lists[0].id,
|
||||
})
|
||||
cy.visit(`/lists/${lists[0].id}/`)
|
||||
|
||||
cy.get('.menu-list li .list-menu-link .color-bubble')
|
||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||
cy.get('.tasks-container .tasks .color-bubble')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Should paginate for > 50 tasks', () => {
|
||||
const tasks = TaskFactory.create(100, {
|
||||
id: '{increment}',
|
||||
title: i => `task${i}`,
|
||||
list_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/list')
|
||||
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('contain', tasks[99].title)
|
||||
|
||||
cy.get('.card-content .pagination .pagination-link')
|
||||
.contains('2')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', '?page=2')
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('contain', tasks[1].title)
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('not.contain', tasks[99].title)
|
||||
})
|
||||
})
|
52
cypress/integration/list/list-view-table.spec.js
Normal file
52
cypress/integration/list/list-view-table.spec.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {TaskFactory} from '../../factories/task'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('List View Table', () => {
|
||||
it('Should show a table with tasks', () => {
|
||||
const tasks = TaskFactory.create(1)
|
||||
cy.visit('/lists/1/table')
|
||||
|
||||
cy.get('.list-table table.table')
|
||||
.should('exist')
|
||||
cy.get('.list-table table.table')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Should have working column switches', () => {
|
||||
TaskFactory.create(1)
|
||||
cy.visit('/lists/1/table')
|
||||
|
||||
cy.get('.list-table .filter-container .items .button')
|
||||
.contains('Columns')
|
||||
.click()
|
||||
cy.get('.list-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')
|
||||
.contains('Done')
|
||||
.click()
|
||||
|
||||
cy.get('.list-table table.table th')
|
||||
.contains('Priority')
|
||||
.should('exist')
|
||||
cy.get('.list-table table.table th')
|
||||
.contains('Done')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Should navigate to the task when the title is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/table')
|
||||
|
||||
cy.get('.list-table table.table')
|
||||
.contains(tasks[0].title)
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', `/tasks/${tasks[0].id}`)
|
||||
})
|
||||
})
|
|
@ -1,25 +1,11 @@
|
|||
import {formatISO, format} from 'date-fns'
|
||||
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {ListFactory} from '../../factories/list'
|
||||
import {UserListFactory} from '../../factories/users_list'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {prepareLists} from './prepareLists'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('Lists', () => {
|
||||
let lists
|
||||
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1)
|
||||
NamespaceFactory.create(1)
|
||||
lists = ListFactory.create(1, {
|
||||
title: 'First List'
|
||||
})
|
||||
TaskFactory.truncate()
|
||||
})
|
||||
prepareLists((newLists) => (lists = newLists))
|
||||
|
||||
it('Should create a new list', () => {
|
||||
cy.visit('/')
|
||||
|
@ -29,7 +15,7 @@ describe('Lists', () => {
|
|||
.contains('New list')
|
||||
.click()
|
||||
cy.url()
|
||||
.should('contain', '/namespaces/1/list')
|
||||
.should('contain', '/lists/new/1')
|
||||
cy.get('.card-header-title')
|
||||
.contains('New list')
|
||||
cy.get('input.input')
|
||||
|
@ -56,7 +42,7 @@ describe('Lists', () => {
|
|||
})
|
||||
|
||||
it('Should rename the list in all places', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
})
|
||||
|
@ -112,429 +98,4 @@ describe('Lists', () => {
|
|||
cy.location('pathname')
|
||||
.should('equal', '/')
|
||||
})
|
||||
|
||||
describe('List View', () => {
|
||||
it('Should be an empty list', () => {
|
||||
cy.visit('/lists/1')
|
||||
cy.url()
|
||||
.should('contain', '/lists/1/list')
|
||||
cy.get('.list-title h1')
|
||||
.should('contain', 'First List')
|
||||
cy.get('.list-title .dropdown')
|
||||
.should('exist')
|
||||
cy.get('p')
|
||||
.contains('This list is currently empty.')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Should navigate to the task when the title is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/list')
|
||||
|
||||
cy.get('.tasks .task .tasktext')
|
||||
.contains(tasks[0].title)
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', `/tasks/${tasks[0].id}`)
|
||||
})
|
||||
|
||||
it('Should not see any elements for a list which is shared read only', () => {
|
||||
UserFactory.create(2)
|
||||
UserListFactory.create(1, {
|
||||
list_id: 2,
|
||||
user_id: 1,
|
||||
right: 0,
|
||||
})
|
||||
const lists = ListFactory.create(2, {
|
||||
owner_id: '{increment}',
|
||||
namespace_id: '{increment}',
|
||||
})
|
||||
cy.visit(`/lists/${lists[1].id}/`)
|
||||
|
||||
cy.get('.list-title a.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, {
|
||||
hex_color: '00db60',
|
||||
})
|
||||
TaskFactory.create(10, {
|
||||
list_id: lists[0].id,
|
||||
})
|
||||
cy.visit(`/lists/${lists[0].id}/`)
|
||||
|
||||
cy.get('.menu-list li .list-menu-link .color-bubble')
|
||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||
cy.get('.tasks-container .tasks .color-bubble')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Should paginate for > 50 tasks', () => {
|
||||
const tasks = TaskFactory.create(100, {
|
||||
id: '{increment}',
|
||||
title: i => `task${i}`,
|
||||
list_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/list')
|
||||
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('contain', tasks[99].title)
|
||||
|
||||
cy.get('.card-content .pagination .pagination-link')
|
||||
.contains('2')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', '?page=2')
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('contain', tasks[1].title)
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('not.contain', tasks[99].title)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Table View', () => {
|
||||
it('Should show a table with tasks', () => {
|
||||
const tasks = TaskFactory.create(1)
|
||||
cy.visit('/lists/1/table')
|
||||
|
||||
cy.get('.table-view table.table')
|
||||
.should('exist')
|
||||
cy.get('.table-view table.table')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Should have working column switches', () => {
|
||||
TaskFactory.create(1)
|
||||
cy.visit('/lists/1/table')
|
||||
|
||||
cy.get('.table-view .filter-container .items .button')
|
||||
.contains('Columns')
|
||||
.click()
|
||||
cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||
.contains('Priority')
|
||||
.click()
|
||||
cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||
.contains('Done')
|
||||
.click()
|
||||
|
||||
cy.get('.table-view table.table th')
|
||||
.contains('Priority')
|
||||
.should('exist')
|
||||
cy.get('.table-view table.table th')
|
||||
.contains('Done')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Should navigate to the task when the title is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/table')
|
||||
|
||||
cy.get('.table-view table.table')
|
||||
.contains(tasks[0].title)
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', `/tasks/${tasks[0].id}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Gantt View', () => {
|
||||
it('Hides tasks with no dates', () => {
|
||||
const tasks = TaskFactory.create(1)
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||
.should('not.contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Shows tasks from the current and next month', () => {
|
||||
const now = new Date()
|
||||
const nextMonth = now
|
||||
nextMonth.setDate(1)
|
||||
nextMonth.setMonth(now.getMonth() + 1)
|
||||
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart-container .gantt-chart .months')
|
||||
.should('contain', format(now, 'MMMM'))
|
||||
.should('contain', format(nextMonth, 'MMMM'))
|
||||
})
|
||||
|
||||
it('Shows tasks with dates', () => {
|
||||
const now = new Date()
|
||||
const tasks = TaskFactory.create(1, {
|
||||
start_date: formatISO(now),
|
||||
end_date: formatISO(now.setDate(now.getDate() + 4))
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||
.should('not.be.empty')
|
||||
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Shows tasks with no dates after enabling them', () => {
|
||||
TaskFactory.create(1, {
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart-container .gantt-options .fancycheckbox')
|
||||
.contains('Show tasks which don\'t have dates set')
|
||||
.click()
|
||||
|
||||
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||
.should('not.be.empty')
|
||||
cy.get('.gantt-chart-container .gantt-chart .tasks .task.nodate')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Drags a task around', () => {
|
||||
const now = new Date()
|
||||
TaskFactory.create(1, {
|
||||
start_date: formatISO(now),
|
||||
end_date: formatISO(now.setDate(now.getDate() + 4))
|
||||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart-container .gantt-chart .tasks .task')
|
||||
.first()
|
||||
.trigger('mousedown', {which: 1})
|
||||
.trigger('mousemove', {clientX: 500, clientY: 0})
|
||||
.trigger('mouseup', {force: true})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Kanban', () => {
|
||||
let buckets
|
||||
|
||||
beforeEach(() => {
|
||||
buckets = BucketFactory.create(2)
|
||||
})
|
||||
|
||||
it('Shows all buckets with their tasks', () => {
|
||||
const data = TaskFactory.create(10, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[0].title)
|
||||
.should('exist')
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[1].title)
|
||||
.should('exist')
|
||||
cy.get('.kanban .bucket')
|
||||
.first()
|
||||
.should('contain', data[0].title)
|
||||
})
|
||||
|
||||
it('Can add a new task to a bucket', () => {
|
||||
const data = TaskFactory.create(2, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket')
|
||||
.contains(buckets[0].title)
|
||||
.get('.bucket-footer .button')
|
||||
.contains('Add another task')
|
||||
.click()
|
||||
cy.get('.kanban .bucket')
|
||||
.contains(buckets[0].title)
|
||||
.get('.bucket-footer .field .control input.input')
|
||||
.type('New Task{enter}')
|
||||
|
||||
cy.get('.kanban .bucket')
|
||||
.first()
|
||||
.should('contain', 'New Task')
|
||||
})
|
||||
|
||||
it('Can create a new bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket.new-bucket .button')
|
||||
.click()
|
||||
cy.get('.kanban .bucket.new-bucket input.input')
|
||||
.type('New Bucket{enter}')
|
||||
|
||||
cy.wait(1000) // Wait for the request to finish
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains('New Bucket')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can set a bucket limit', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||
.contains('Limit: Not Set')
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input')
|
||||
.first()
|
||||
.type(3)
|
||||
cy.get('[data-cy="setBucketLimit"]')
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header span.limit')
|
||||
.contains('0/3')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can rename a bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header .title')
|
||||
.first()
|
||||
.type('{selectall}New Bucket Title{enter}')
|
||||
cy.get('.kanban .bucket .bucket-header .title')
|
||||
.first()
|
||||
.should('contain', 'New Bucket Title')
|
||||
})
|
||||
|
||||
it('Can delete a bucket', () => {
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||
.contains('Delete')
|
||||
.click()
|
||||
cy.get('.modal-mask .modal-container .modal-content .header')
|
||||
.should('contain', 'Delete the bucket')
|
||||
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
||||
.contains('Do it!')
|
||||
.click()
|
||||
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[0].title)
|
||||
.should('not.exist')
|
||||
cy.get('.kanban .bucket .title')
|
||||
.contains(buckets[1].title)
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can drag tasks around', () => {
|
||||
const tasks = TaskFactory.create(2, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
.first()
|
||||
.drag('.kanban .bucket:nth-child(2) .tasks .dropper')
|
||||
|
||||
cy.get('.kanban .bucket:nth-child(2) .tasks')
|
||||
.should('contain', tasks[0].title)
|
||||
cy.get('.kanban .bucket:nth-child(1) .tasks')
|
||||
.should('not.contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Should navigate to the task when the task card is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', `/tasks/${tasks[0].id}`)
|
||||
})
|
||||
|
||||
it('Should remove a task from the kanban board when moving it to another list', () => {
|
||||
const lists = ListFactory.create(2)
|
||||
BucketFactory.create(2, {
|
||||
list_id: '{increment}',
|
||||
})
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
const task = tasks[0]
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getSettled('.kanban .bucket .tasks .task')
|
||||
.contains(task.title)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Move task')
|
||||
.click()
|
||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
|
||||
.type(`${lists[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')
|
||||
.children()
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification', { timeout: 1000 })
|
||||
.should('contain', 'Success')
|
||||
cy.go('back')
|
||||
cy.get('.kanban .bucket')
|
||||
.should('not.contain', task.title)
|
||||
})
|
||||
})
|
||||
|
||||
describe('List history', () => {
|
||||
it('should show a list history on the home page', () => {
|
||||
const lists = ListFactory.create(6)
|
||||
|
||||
cy.visit('/')
|
||||
cy.get('h3')
|
||||
.contains('Last viewed')
|
||||
.should('not.exist')
|
||||
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
cy.visit(`/lists/${lists[1].id}`)
|
||||
cy.visit(`/lists/${lists[2].id}`)
|
||||
cy.visit(`/lists/${lists[3].id}`)
|
||||
cy.visit(`/lists/${lists[4].id}`)
|
||||
cy.visit(`/lists/${lists[5].id}`)
|
||||
|
||||
cy.visit('/')
|
||||
cy.get('h3')
|
||||
.contains('Last viewed')
|
||||
.should('exist')
|
||||
cy.get('.list-cards-wrapper-2-rows')
|
||||
.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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
16
cypress/integration/list/prepareLists.js
Normal file
16
cypress/integration/list/prepareLists.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {ListFactory} from '../../factories/list'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
|
||||
export function prepareLists(setLists = () => {}) {
|
||||
beforeEach(() => {
|
||||
UserFactory.create(1)
|
||||
NamespaceFactory.create(1)
|
||||
const lists = ListFactory.create(1, {
|
||||
title: 'First List'
|
||||
})
|
||||
setLists(lists)
|
||||
TaskFactory.truncate()
|
||||
})
|
||||
}
|
|
@ -116,6 +116,7 @@ describe('Task', () => {
|
|||
.should('be.visible')
|
||||
.should('contain', 'Done')
|
||||
cy.get('.task-view .action-buttons p.created')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.should('contain', 'Done')
|
||||
})
|
||||
|
@ -372,13 +373,13 @@ describe('Task', () => {
|
|||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
.should('be.visible')
|
||||
.should('contain', labels[0].title)
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
.children()
|
||||
.first()
|
||||
.get('a.delete')
|
||||
.get('[data-cy="taskDetail.removeLabel"]')
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
|
|
|
@ -25,7 +25,6 @@ context('Registration', () => {
|
|||
cy.get('#username').type(fixture.username)
|
||||
cy.get('#email').type(fixture.email)
|
||||
cy.get('#password').type(fixture.password)
|
||||
cy.get('#passwordValidation').type(fixture.password)
|
||||
cy.get('#register-submit').click()
|
||||
cy.url().should('include', '/')
|
||||
cy.clock(1625656161057) // 13:00
|
||||
|
@ -43,7 +42,6 @@ context('Registration', () => {
|
|||
cy.get('#username').type(fixture.username)
|
||||
cy.get('#email').type(fixture.email)
|
||||
cy.get('#password').type(fixture.password)
|
||||
cy.get('#passwordValidation').type(fixture.password)
|
||||
cy.get('#register-submit').click()
|
||||
cy.get('div.message.danger').contains('A user with this username already exists.')
|
||||
})
|
||||
|
|
|
@ -8,12 +8,14 @@ describe('User Settings', () => {
|
|||
})
|
||||
|
||||
it('Changes the user avatar', () => {
|
||||
cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar')
|
||||
|
||||
cy.visit('/user/settings/avatar')
|
||||
|
||||
cy.get('input[name=avatarProvider][value=upload]')
|
||||
.click()
|
||||
cy.get('input[type=file]', {timeout: 1000})
|
||||
.attachFile('image.jpg')
|
||||
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
|
||||
cy.get('.vue-handler-wrapper.vue-handler-wrapper--south .vue-simple-handler.vue-simple-handler--south')
|
||||
.trigger('mousedown', {which: 1})
|
||||
.trigger('mousemove', {clientY: 100})
|
||||
|
@ -22,7 +24,7 @@ describe('User Settings', () => {
|
|||
.contains('Upload Avatar')
|
||||
.click()
|
||||
|
||||
cy.wait(3000) // Wait for the request to finish
|
||||
cy.wait('@uploadAvatar')
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
import './commands'
|
||||
import 'cypress-file-upload'
|
||||
import '@4tw/cypress-drag-drop'
|
||||
|
||||
// see https://github.com/cypress-io/cypress/issues/702#issuecomment-587127275
|
||||
|
|
79
package.json
79
package.json
|
@ -18,37 +18,37 @@
|
|||
"browserslist:update": "npx browserslist@latest --update-db"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/hotkey": "1.6.1",
|
||||
"@github/hotkey": "2.0.0",
|
||||
"@kyvg/vue3-notification": "2.3.4",
|
||||
"@sentry/tracing": "6.16.1",
|
||||
"@sentry/vue": "6.16.1",
|
||||
"@sentry/tracing": "6.17.4",
|
||||
"@sentry/vue": "6.17.4",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@vue/compat": "3.2.26",
|
||||
"@vueuse/core": "7.5.2",
|
||||
"@vueuse/router": "7.5.3",
|
||||
"@vue/compat": "3.2.29",
|
||||
"@vueuse/core": "7.5.5",
|
||||
"@vueuse/router": "7.5.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.65.0",
|
||||
"codemirror": "5.65.1",
|
||||
"copy-to-clipboard": "3.3.1",
|
||||
"date-fns": "2.28.0",
|
||||
"dompurify": "2.3.4",
|
||||
"easymde": "2.15.0",
|
||||
"dompurify": "2.3.5",
|
||||
"easymde": "2.16.1",
|
||||
"flatpickr": "4.6.9",
|
||||
"flexsearch": "0.7.21",
|
||||
"highlight.js": "11.4.0",
|
||||
"is-touch-device": "1.0.1",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"marked": "4.0.9",
|
||||
"marked": "4.0.12",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"ufo": "0.7.9",
|
||||
"v-tooltip": "4.0.0-beta.13",
|
||||
"vue": "3.2.26",
|
||||
"vue-advanced-cropper": "2.7.1",
|
||||
"ufo": "0.7.10",
|
||||
"v-tooltip": "4.0.0-beta.17",
|
||||
"vue": "3.2.29",
|
||||
"vue-advanced-cropper": "2.8.0",
|
||||
"vue-drag-resize": "2.0.3",
|
||||
"vue-flatpickr-component": "9.0.5",
|
||||
"vue-i18n": "9.2.0-beta.26",
|
||||
"vue-i18n": "9.2.0-beta.30",
|
||||
"vue-router": "4.0.12",
|
||||
"vuedraggable": "4.1.0",
|
||||
"vuex": "4.0.2",
|
||||
|
@ -56,41 +56,40 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.1.0",
|
||||
"@faker-js/faker": "6.0.0-alpha.5",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
||||
"@types/flexsearch": "0.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.9.0",
|
||||
"@typescript-eslint/parser": "5.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.10.2",
|
||||
"@typescript-eslint/parser": "5.10.2",
|
||||
"@vitejs/plugin-legacy": "1.6.4",
|
||||
"@vitejs/plugin-vue": "2.0.1",
|
||||
"@vitejs/plugin-vue": "2.1.0",
|
||||
"@vue/eslint-config-typescript": "10.0.0",
|
||||
"autoprefixer": "10.4.2",
|
||||
"axios": "0.24.0",
|
||||
"axios": "0.25.0",
|
||||
"browserslist": "4.19.1",
|
||||
"caniuse-lite": "1.0.30001298",
|
||||
"cypress": "9.2.0",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"esbuild": "0.14.11",
|
||||
"eslint": "8.6.0",
|
||||
"eslint-plugin-vue": "8.2.0",
|
||||
"caniuse-lite": "1.0.30001307",
|
||||
"cypress": "9.4.1",
|
||||
"esbuild": "0.14.18",
|
||||
"eslint": "8.8.0",
|
||||
"eslint-plugin-vue": "8.4.1",
|
||||
"express": "4.17.2",
|
||||
"faker": "5.5.3",
|
||||
"netlify-cli": "8.6.15",
|
||||
"happy-dom": "2.25.1",
|
||||
"postcss": "8.4.5",
|
||||
"postcss-preset-env": "7.2.0",
|
||||
"rollup": "2.63.0",
|
||||
"rollup-plugin-visualizer": "5.5.2",
|
||||
"sass": "1.47.0",
|
||||
"happy-dom": "2.31.1",
|
||||
"netlify-cli": "8.15.0",
|
||||
"postcss": "8.4.6",
|
||||
"postcss-preset-env": "7.3.1",
|
||||
"rollup": "2.67.0",
|
||||
"rollup-plugin-visualizer": "5.5.4",
|
||||
"sass": "1.49.7",
|
||||
"slugify": "1.6.5",
|
||||
"typescript": "4.5.4",
|
||||
"vite": "2.7.10",
|
||||
"vite-plugin-pwa": "0.11.12",
|
||||
"vite-svg-loader": "3.1.1",
|
||||
"vitest": "0.0.139",
|
||||
"vue-tsc": "0.30.2",
|
||||
"typescript": "4.5.5",
|
||||
"vite": "2.7.13",
|
||||
"vite-plugin-pwa": "0.11.13",
|
||||
"vite-svg-loader": "3.1.2",
|
||||
"vitest": "0.2.7",
|
||||
"vue-tsc": "0.31.1",
|
||||
"wait-on": "6.0.0",
|
||||
"workbox-cli": "6.4.2"
|
||||
},
|
||||
|
@ -130,7 +129,7 @@
|
|||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"ecmaVersion": 2021
|
||||
"ecmaVersion": 2022
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"*.test.*",
|
||||
|
|
|
@ -3,5 +3,11 @@
|
|||
"labels": ["dependencies"],
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["netlify-cli"],
|
||||
"extends": ["schedule:weekly"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ import {useBodyClass} from '@/composables/useBodyClass'
|
|||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
useBodyClass('is-touch', isTouchDevice)
|
||||
useBodyClass('is-touch', isTouchDevice())
|
||||
const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive)
|
||||
|
||||
const authUser = computed(() => store.getters['auth/authUser'])
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.5 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
@ -1,33 +1,51 @@
|
|||
<template>
|
||||
<div>
|
||||
<a @click="$store.commit('menuActive', false)" class="menu-hide-button" v-if="menuActive">
|
||||
<BaseButton
|
||||
v-if="menuActive"
|
||||
@click="$store.commit('menuActive', false)"
|
||||
class="menu-hide-button"
|
||||
>
|
||||
<icon icon="times" />
|
||||
</a>
|
||||
</BaseButton>
|
||||
<div
|
||||
:class="{'has-background': background}"
|
||||
:style="{'background-image': background && `url(${background})`}"
|
||||
class="app-container"
|
||||
>
|
||||
<navigation/>
|
||||
<div
|
||||
<main
|
||||
:class="[
|
||||
{ 'is-menu-enabled': menuActive },
|
||||
$route.name,
|
||||
]"
|
||||
class="app-content"
|
||||
>
|
||||
<a @click="$store.commit('menuActive', false)" class="mobile-overlay" v-if="menuActive"></a>
|
||||
<BaseButton
|
||||
v-if="menuActive"
|
||||
@click="$store.commit('menuActive', false)"
|
||||
class="mobile-overlay"
|
||||
/>
|
||||
|
||||
<quick-actions/>
|
||||
|
||||
<router-view/>
|
||||
|
||||
<router-view name="popup" v-slot="{ Component }">
|
||||
<transition name="modal">
|
||||
<router-view :route="routeWithModal" v-slot="{ Component }">
|
||||
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
<transition name="modal">
|
||||
<modal
|
||||
v-if="currentModal"
|
||||
@close="closeModal()"
|
||||
variant="scrolling"
|
||||
class="task-detail-view-modal"
|
||||
>
|
||||
<component :is="currentModal" />
|
||||
</modal>
|
||||
</transition>
|
||||
|
||||
<a
|
||||
class="keyboard-shortcuts-button"
|
||||
@click="showKeyboardShortcuts()"
|
||||
|
@ -35,13 +53,13 @@
|
|||
>
|
||||
<icon icon="keyboard"/>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {watch, computed} from 'vue'
|
||||
import {watch, computed, shallowRef, watchEffect, VNode, h} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useEventListener} from '@vueuse/core'
|
||||
|
@ -49,6 +67,59 @@ import {useEventListener} from '@vueuse/core'
|
|||
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
|
||||
import Navigation from '@/components/home/navigation.vue'
|
||||
import QuickActions from '@/components/quick-actions/quick-actions.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
function useRouteWithModal() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
|
||||
|
||||
const routeWithModal = computed(() => {
|
||||
return backdropView.value
|
||||
? router.resolve(backdropView.value)
|
||||
: route
|
||||
})
|
||||
|
||||
const currentModal = shallowRef<VNode>()
|
||||
watchEffect(() => {
|
||||
if (!backdropView.value) {
|
||||
currentModal.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
// logic from vue-router
|
||||
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
|
||||
const routePropsOption = route.matched[0]?.props.default
|
||||
const routeProps = routePropsOption
|
||||
? routePropsOption === true
|
||||
? route.params
|
||||
: typeof routePropsOption === 'function'
|
||||
? routePropsOption(route)
|
||||
: routePropsOption
|
||||
: null
|
||||
|
||||
currentModal.value = h(
|
||||
route.matched[0]?.components.default,
|
||||
routeProps,
|
||||
)
|
||||
})
|
||||
|
||||
function closeModal() {
|
||||
const historyState = computed(() => route.fullPath && window.history.state)
|
||||
|
||||
if (historyState.value) {
|
||||
router.back()
|
||||
} else {
|
||||
const backdropRoute = historyState.value?.backdropView && router.resolve(historyState.value.backdropView)
|
||||
router.push(backdropRoute)
|
||||
}
|
||||
}
|
||||
|
||||
return { routeWithModal, currentModal, closeModal }
|
||||
}
|
||||
|
||||
const { routeWithModal, currentModal, closeModal } = useRouteWithModal()
|
||||
|
||||
|
||||
const store = useStore()
|
||||
|
||||
|
@ -223,4 +294,6 @@ store.dispatch('labels/loadAllLabels')
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div :class="{'is-active': menuActive}" class="namespace-container">
|
||||
<div class="menu top-menu">
|
||||
<aside :class="{'is-active': menuActive}" class="namespace-container">
|
||||
<nav class="menu top-menu">
|
||||
<router-link :to="{name: 'home'}" class="logo">
|
||||
<Logo width="164" height="48"/>
|
||||
</router-link>
|
||||
|
@ -46,31 +46,35 @@
|
|||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
|
||||
<nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
|
||||
<template v-for="(n, nk) in namespaces" :key="n.id">
|
||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
||||
<span
|
||||
@click="toggleLists(n.id)"
|
||||
class="menu-label"
|
||||
v-tooltip="namespaceTitles[nk]">
|
||||
<span class="name">
|
||||
v-tooltip="namespaceTitles[nk]"
|
||||
>
|
||||
<span
|
||||
v-if="n.hexColor !== ''"
|
||||
:style="{ backgroundColor: n.hexColor }"
|
||||
class="color-bubble"
|
||||
v-if="n.hexColor !== ''">
|
||||
</span>
|
||||
/>
|
||||
<span class="name">
|
||||
{{ namespaceTitles[nk] }}
|
||||
</span>
|
||||
</span>
|
||||
<a
|
||||
class="icon is-small toggle-lists-icon"
|
||||
class="icon is-small toggle-lists-icon pl-2"
|
||||
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
|
||||
@click="toggleLists(n.id)"
|
||||
>
|
||||
<icon icon="chevron-down"/>
|
||||
</a>
|
||||
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
|
||||
({{ namespaceListsCount[nk] }})
|
||||
</span>
|
||||
</span>
|
||||
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -86,13 +90,15 @@
|
|||
v-bind="dragOptions"
|
||||
:modelValue="activeLists[nk]"
|
||||
@update:modelValue="(lists) => updateActiveLists(n, lists)"
|
||||
:group="`namespace-${n.id}-lists`"
|
||||
group="namespace-lists"
|
||||
@start="() => drag = true"
|
||||
@end="e => saveListPosition(e, nk)"
|
||||
@end="saveListPosition"
|
||||
handle=".handle"
|
||||
:disabled="n.id < 0 || null"
|
||||
tag="transition-group"
|
||||
item-key="id"
|
||||
:data-namespace-id="n.id"
|
||||
:data-namespace-index="nk"
|
||||
:component-data="{
|
||||
type: 'transition',
|
||||
tag: 'ul',
|
||||
|
@ -145,9 +151,9 @@
|
|||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
</nav>
|
||||
<PoweredByLink/>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -194,13 +200,13 @@ export default {
|
|||
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
|
||||
}),
|
||||
activeLists() {
|
||||
return this.namespaces.map(({lists}) => lists?.filter(item => !item.isArchived))
|
||||
return this.namespaces.map(({lists}) => lists?.filter(item => typeof item !== 'undefined' && !item.isArchived))
|
||||
},
|
||||
namespaceTitles() {
|
||||
return this.namespaces.map((namespace, index) => {
|
||||
const title = this.getNamespaceTitle(namespace)
|
||||
return `${title} (${this.activeLists[index]?.length ?? 0})`
|
||||
})
|
||||
return this.namespaces.map((namespace) => this.getNamespaceTitle(namespace))
|
||||
},
|
||||
namespaceListsCount() {
|
||||
return this.namespaces.map((_, index) => this.activeLists[index]?.length ?? 0)
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
|
@ -237,15 +243,15 @@ export default {
|
|||
this.listsVisible[namespaceId] = !this.listsVisible[namespaceId]
|
||||
},
|
||||
updateActiveLists(namespace, activeLists) {
|
||||
// this is a bit hacky: since we do have to filter out the archived items from the list
|
||||
// This is a bit hacky: since we do have to filter out the archived items from the list
|
||||
// for vue draggable updating it is not as simple as replacing it.
|
||||
// instead we iterate over the non archived items in the old list and replace them with the ones in their new order
|
||||
const lists = namespace.lists.map((item) => {
|
||||
if (item.isArchived) {
|
||||
return item
|
||||
}
|
||||
return activeLists.shift()
|
||||
})
|
||||
// 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
|
||||
// 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 newNamespace = {
|
||||
...namespace,
|
||||
|
@ -255,8 +261,11 @@ export default {
|
|||
this.$store.commit('namespaces/setNamespaceById', newNamespace)
|
||||
},
|
||||
|
||||
async saveListPosition(e, namespaceIndex) {
|
||||
const listsActive = this.activeLists[namespaceIndex]
|
||||
async saveListPosition(e) {
|
||||
const namespaceId = parseInt(e.to.dataset.namespaceId)
|
||||
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex)
|
||||
|
||||
const listsActive = this.activeLists[newNamespaceIndex]
|
||||
const list = listsActive[e.newIndex]
|
||||
const listBefore = listsActive[e.newIndex - 1] ?? null
|
||||
const listAfter = listsActive[e.newIndex + 1] ?? null
|
||||
|
@ -269,6 +278,7 @@ export default {
|
|||
await this.$store.dispatch('lists/updateList', {
|
||||
...list,
|
||||
position,
|
||||
namespaceId,
|
||||
})
|
||||
} finally {
|
||||
this.listUpdating[list.id] = false
|
||||
|
@ -365,8 +375,9 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
|
||||
.menu-label {
|
||||
.color-bubble {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.is-archived {
|
||||
|
@ -387,6 +398,12 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--grey-500);
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<template>
|
||||
<nav
|
||||
<header
|
||||
:class="{'has-background': background}"
|
||||
aria-label="main navigation"
|
||||
class="navbar main-theme is-fixed-top"
|
||||
role="navigation"
|
||||
>
|
||||
<router-link :to="{name: 'home'}" class="logo-link">
|
||||
<Logo width="164" height="48"/>
|
||||
|
@ -77,7 +76,7 @@
|
|||
</dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -85,4 +85,8 @@ export default {
|
|||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .update-notification {
|
||||
color: var(--grey-200);
|
||||
}
|
||||
</style>
|
85
src/components/input/password.vue
Normal file
85
src/components/input/password.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<div class="password-field">
|
||||
<input
|
||||
class="input"
|
||||
id="password"
|
||||
name="password"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
required
|
||||
:type="passwordFieldType"
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="e => $emit('submit', e)"
|
||||
:tabindex="props.tabindex"
|
||||
@focusout="validate"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<a
|
||||
@click="togglePasswordFieldType"
|
||||
class="password-field-type-toggle"
|
||||
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
||||
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
||||
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
||||
</a>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="!isValid">
|
||||
{{ $t('user.auth.passwordRequired') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watch} from 'vue'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
tabindex: String,
|
||||
modelValue: String,
|
||||
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
|
||||
validateInitially: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submit', 'update:modelValue'])
|
||||
|
||||
const passwordFieldType = ref<String>('password')
|
||||
const password = ref<String>('')
|
||||
const isValid = ref<Boolean>(!props.validateInitially)
|
||||
|
||||
watch(
|
||||
() => props.validateInitially,
|
||||
(doValidate: Boolean) => {
|
||||
if (doValidate) {
|
||||
validate()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function validate() {
|
||||
useDebounceFn(() => {
|
||||
isValid.value = password.value !== ''
|
||||
}, 100)()
|
||||
}
|
||||
|
||||
function togglePasswordFieldType() {
|
||||
passwordFieldType.value = passwordFieldType.value === 'password'
|
||||
? 'text'
|
||||
: 'password'
|
||||
}
|
||||
|
||||
function handleInput(e) {
|
||||
password.value = e.target.value
|
||||
emit('update:modelValue', e.target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.password-field {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-field-type-toggle {
|
||||
position: absolute;
|
||||
color: var(--grey-400);
|
||||
top: 50%;
|
||||
right: 1rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
</style>
|
|
@ -2,21 +2,22 @@
|
|||
<dropdown>
|
||||
<template v-if="isSavedFilter">
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
|
||||
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
|
||||
:to="{ name: 'filter.settings.delete', params: { listId: list.id } }"
|
||||
icon="trash-alt"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
</dropdown-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="list.isArchived">
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
|
||||
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
|
@ -24,37 +25,38 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
|
||||
:to="{ name: 'list.settings.edit', params: { listId: list.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.background`, params: { listId: list.id } }"
|
||||
v-if="backgroundsEnabled"
|
||||
:to="{ name: 'list.settings.background', params: { listId: list.id } }"
|
||||
icon="image"
|
||||
>
|
||||
{{ $t('menu.setBackground') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.share`, params: { listId: list.id } }"
|
||||
:to="{ name: 'list.settings.share', params: { listId: list.id } }"
|
||||
icon="share-alt"
|
||||
>
|
||||
{{ $t('menu.share') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.duplicate`, params: { listId: list.id } }"
|
||||
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }"
|
||||
icon="paste"
|
||||
>
|
||||
{{ $t('menu.duplicate') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
|
||||
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
</dropdown-item>
|
||||
<task-subscription
|
||||
v-if="subscription"
|
||||
class="dropdown-item has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="list"
|
||||
|
@ -63,7 +65,7 @@
|
|||
@change="sub => subscription = sub"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
|
||||
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
>
|
||||
|
@ -73,56 +75,32 @@
|
|||
</dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watchEffect} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
|
||||
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import TaskSubscription from '@/components/misc/subscription.vue'
|
||||
import ListModel from '@/models/list'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
export default {
|
||||
name: 'list-settings-dropdown',
|
||||
data() {
|
||||
return {
|
||||
subscription: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TaskSubscription,
|
||||
DropdownItem,
|
||||
Dropdown,
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: ListModel,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.subscription = this.list.subscription
|
||||
},
|
||||
computed: {
|
||||
backgroundsEnabled() {
|
||||
return this.$store.state.config.enabledBackgroundProviders !== null && this.$store.state.config.enabledBackgroundProviders.length > 0
|
||||
},
|
||||
listRoutePrefix() {
|
||||
let name = 'list'
|
||||
})
|
||||
|
||||
|
||||
if (this.$route.name !== null && this.$route.name.startsWith('list.')) {
|
||||
// HACK: we should implement a better routing for the modals
|
||||
const settingsRoutes = ['edit', 'delete', 'archive', 'background', 'share', 'duplicate']
|
||||
const suffix = settingsRoutes.find((route) => this.$route.name.endsWith(`.settings.${route}`))
|
||||
name = this.$route.name.replace(`.settings.${suffix}`,'')
|
||||
const subscription = ref<SubscriptionModel>()
|
||||
watchEffect(() => {
|
||||
if (props.list.subscription) {
|
||||
subscription.value = props.list.subscription
|
||||
}
|
||||
})
|
||||
|
||||
if (this.isSavedFilter) {
|
||||
name = name.replace('list.', 'filter.')
|
||||
}
|
||||
|
||||
return `${name}.settings`
|
||||
},
|
||||
isSavedFilter() {
|
||||
return getSavedFilterIdFromListId(this.list.id) > 0
|
||||
},
|
||||
},
|
||||
}
|
||||
const store = useStore()
|
||||
const backgroundsEnabled = computed(() => store.state.config.enabledBackgroundProviders?.length > 0)
|
||||
const isSavedFilter = computed(() => getSavedFilterIdFromListId(props.list.id) > 0)
|
||||
</script>
|
||||
|
|
|
@ -29,9 +29,10 @@
|
|||
|
||||
<script>
|
||||
import Filters from '@/components/list/partials/filters'
|
||||
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
|
||||
import Popup from '@/components/misc/popup'
|
||||
|
||||
import {getDefaultParams} from '@/composables/taskList'
|
||||
|
||||
export default {
|
||||
name: 'filter-popup',
|
||||
components: {
|
||||
|
|
|
@ -191,7 +191,7 @@ import NamespaceService from '@/services/namespace'
|
|||
import EditLabels from '@/components/tasks/partials/editLabels.vue'
|
||||
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
|
||||
import {getDefaultParams} from '@/composables/taskList'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskList.js
|
||||
const DEFAULT_PARAMS = {
|
||||
|
|
|
@ -28,19 +28,20 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watch} from 'vue'
|
||||
import {PropType, ref, watch} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
import ListModel from '@/models/list'
|
||||
|
||||
const background = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Object,
|
||||
type: Object as PropType<ListModel>,
|
||||
required: true,
|
||||
},
|
||||
showArchived: {
|
||||
|
@ -68,7 +69,7 @@ async function loadBackground() {
|
|||
|
||||
const store = useStore()
|
||||
|
||||
function toggleFavoriteList(list) {
|
||||
function toggleFavoriteList(list: ListModel) {
|
||||
// The favorites pseudo list is always favorite
|
||||
// Archived lists cannot be marked favorite
|
||||
if (list.id === -1 || list.isArchived) {
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
<template v-for="(s, i) in shortcuts" :key="i">
|
||||
<h3>{{ $t(s.title) }}</h3>
|
||||
|
||||
<message>
|
||||
<message class="mb-4" v-if="s.available">
|
||||
{{
|
||||
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
|
||||
s.available($route)
|
||||
? $t('keyboardShortcuts.currentPageOnly')
|
||||
: $t('keyboardShortcuts.allPages')
|
||||
}}
|
||||
</message>
|
||||
|
||||
|
@ -17,7 +19,8 @@
|
|||
class="shortcut-keys"
|
||||
is="dd"
|
||||
:keys="sc.keys"
|
||||
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
|
||||
:combination="sc.combination && $t(`keyboardShortcuts.${sc.combination}`)"
|
||||
/>
|
||||
</template>
|
||||
</dl>
|
||||
</template>
|
||||
|
@ -25,28 +28,18 @@
|
|||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
|
||||
import Shortcut from '@/components/misc/shortcut.vue'
|
||||
import Message from '@/components/misc/message'
|
||||
import {KEYBOARD_SHORTCUTS} from './shortcuts'
|
||||
<script lang="ts" setup>
|
||||
import {useStore} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'keyboard-shortcuts',
|
||||
components: {
|
||||
Message,
|
||||
Shortcut,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shortcuts: KEYBOARD_SHORTCUTS,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
|
||||
},
|
||||
},
|
||||
import Shortcut from '@/components/misc/shortcut.vue'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
|
||||
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
|
||||
import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts'
|
||||
|
||||
const store = useStore()
|
||||
function close() {
|
||||
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,11 +1,24 @@
|
|||
import {RouteLocation} from 'vue-router'
|
||||
|
||||
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||
|
||||
const ctrl = isAppleDevice() ? '⌘' : 'ctrl'
|
||||
|
||||
export const KEYBOARD_SHORTCUTS = [
|
||||
interface Shortcut {
|
||||
title: string
|
||||
keys: string[]
|
||||
combination?: 'then'
|
||||
}
|
||||
|
||||
interface ShortcutGroup {
|
||||
title: string
|
||||
available?: (route: RouteLocation) => boolean
|
||||
shortcuts: Shortcut[]
|
||||
}
|
||||
|
||||
export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
|
||||
{
|
||||
title: 'keyboardShortcuts.general',
|
||||
available: () => null,
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.toggleMenu',
|
||||
|
@ -29,7 +42,7 @@ export const KEYBOARD_SHORTCUTS = [
|
|||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.title',
|
||||
available: (route) => route.name.startsWith('list.'),
|
||||
available: (route) => (route.name as string)?.startsWith('list.'),
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToListView',
|
||||
|
@ -55,13 +68,7 @@ export const KEYBOARD_SHORTCUTS = [
|
|||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.title',
|
||||
available: (route) => [
|
||||
'task.detail',
|
||||
'task.list.detail',
|
||||
'task.gantt.detail',
|
||||
'task.kanban.detail',
|
||||
'task.detail',
|
||||
].includes(route.name),
|
||||
available: (route) => route.name === 'task.detail',
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.task.assign',
|
|
@ -1,18 +1,35 @@
|
|||
<template>
|
||||
<div class="message-wrapper">
|
||||
<div class="message" :class="variant">
|
||||
<div class="message" :class="[variant, textAlignClass]">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
import {computed, PropType} from 'vue'
|
||||
|
||||
const TEXT_ALIGN_MAP = Object.freeze({
|
||||
left: '',
|
||||
center: 'has-text-centered',
|
||||
right: 'has-text-right',
|
||||
})
|
||||
|
||||
type textAlignVariants = keyof typeof TEXT_ALIGN_MAP
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'info',
|
||||
},
|
||||
textAlign: {
|
||||
type: String as PropType<textAlignVariants>,
|
||||
default: 'left',
|
||||
},
|
||||
})
|
||||
|
||||
const textAlignClass = computed(() => TEXT_ALIGN_MAP[props.textAlign])
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
<div>
|
||||
<h2 class="title" v-if="title">{{ title }}</h2>
|
||||
<api-config/>
|
||||
<Message v-if="motd !== ''" class="is-hidden-tablet mb-4">
|
||||
{{ motd }}
|
||||
</Message>
|
||||
<slot/>
|
||||
</div>
|
||||
<legal/>
|
||||
|
@ -38,8 +41,8 @@ const store = useStore()
|
|||
const {t} = useI18n()
|
||||
|
||||
const motd = computed(() => store.state.config.motd)
|
||||
// @ts-ignore
|
||||
const title = computed(() => t(route.meta.title ?? ''))
|
||||
|
||||
const title = computed(() => t(route.meta?.title as string || ''))
|
||||
useTitle(() => title.value)
|
||||
</script>
|
||||
|
||||
|
|
|
@ -52,9 +52,15 @@ import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
|||
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
||||
import {useOnline} from '@/composables/useOnline'
|
||||
|
||||
import {useRouter, useRoute} from 'vue-router'
|
||||
import {getAuthForRoute} from '@/router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const ready = computed(() => store.state.vikunjaReady)
|
||||
const ready = ref(false)
|
||||
const online = useOnline()
|
||||
|
||||
const error = ref('')
|
||||
|
@ -63,6 +69,11 @@ const showLoading = computed(() => !ready.value && error.value === '')
|
|||
async function load() {
|
||||
try {
|
||||
await store.dispatch('loadApp')
|
||||
const redirectTo = getAuthForRoute(route)
|
||||
if (typeof redirectTo !== 'undefined') {
|
||||
await router.push(redirectTo)
|
||||
}
|
||||
ready.value = true
|
||||
} catch (e: any) {
|
||||
error.value = e
|
||||
}
|
||||
|
|
|
@ -1,53 +1,51 @@
|
|||
<template>
|
||||
<x-button
|
||||
v-if="isButton"
|
||||
variant="secondary"
|
||||
:icon="icon"
|
||||
:icon="iconName"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:disabled="disabled || null"
|
||||
v-if="isButton"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
<a
|
||||
<BaseButton
|
||||
v-else
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:class="{'is-disabled': disabled}"
|
||||
v-else
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="icon"/>
|
||||
<icon :icon="iconName"/>
|
||||
</span>
|
||||
{{ buttonText }}
|
||||
</a>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, shallowRef} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import SubscriptionService from '@/services/subscription'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
import {success} from '@/message'
|
||||
|
||||
const props = defineProps({
|
||||
entity: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
subscription: {
|
||||
required: true,
|
||||
},
|
||||
entityId: {
|
||||
required: true,
|
||||
},
|
||||
isButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
interface Props {
|
||||
entity: string
|
||||
entityId: number
|
||||
subscription: SubscriptionModel
|
||||
isButton?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isButton: true,
|
||||
})
|
||||
|
||||
const subscriptionEntity = computed<string>(() => props.subscription.entity)
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const subscriptionService = shallowRef(new SubscriptionService())
|
||||
|
@ -57,7 +55,7 @@ const tooltipText = computed(() => {
|
|||
if (disabled.value) {
|
||||
return t('task.subscription.subscribedThroughParent', {
|
||||
entity: props.entity,
|
||||
parent: props.subscription.entity,
|
||||
parent: subscriptionEntity.value,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -67,13 +65,13 @@ const tooltipText = computed(() => {
|
|||
})
|
||||
|
||||
const buttonText = computed(() => props.subscription !== null ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
|
||||
const icon = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
|
||||
const iconName = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
|
||||
const disabled = computed(() => {
|
||||
if (props.subscription === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return props.subscription.entity !== props.entity
|
||||
return subscriptionEntity.value !== props.entity
|
||||
})
|
||||
|
||||
function changeSubscription() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<template>
|
||||
<!-- FIXME: transition should not be included in the modal -->
|
||||
<transition name="modal">
|
||||
<section
|
||||
v-if="enabled"
|
||||
|
@ -21,6 +22,13 @@
|
|||
'is-wide': wide
|
||||
}"
|
||||
>
|
||||
<BaseButton
|
||||
@click="emit('close')"
|
||||
class="close"
|
||||
>
|
||||
<icon icon="times"/>
|
||||
</BaseButton>
|
||||
|
||||
<slot>
|
||||
<div class="header">
|
||||
<slot name="header"></slot>
|
||||
|
@ -53,6 +61,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
export const TRANSITION_NAMES = {
|
||||
MODAL: 'modal',
|
||||
FADE: 'fade',
|
||||
|
@ -70,6 +80,11 @@ function validValue(values) {
|
|||
|
||||
export default {
|
||||
name: 'modal',
|
||||
|
||||
components: {
|
||||
BaseButton,
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Close the model when escape is pressed
|
||||
|
@ -197,17 +212,22 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
right: 26px;
|
||||
color: var(--white);
|
||||
font-size: 2rem;
|
||||
|
||||
|
||||
/* Transitions */
|
||||
|
||||
.modal-enter,
|
||||
.modal-leave-active {
|
||||
opacity: 0;
|
||||
@media screen and (max-width: $desktop) {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave-active .modal-container {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// Close icon SVG uses currentColor, change the color to keep it visible
|
||||
.dark .task-detail-view-modal .close {
|
||||
color: var(--grey-900);
|
||||
}
|
||||
</style>
|
|
@ -13,6 +13,7 @@
|
|||
import {ref, computed} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
|
||||
const emit = defineEmits(['selected'])
|
||||
|
||||
|
@ -25,7 +26,7 @@ function findNamespaces(newQuery: string) {
|
|||
query.value = newQuery
|
||||
}
|
||||
|
||||
function select(namespace) {
|
||||
function select(namespace: NamespaceModel) {
|
||||
emit('selected', namespace)
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.share', params: { id: namespace.id } }"
|
||||
:to="{ name: 'namespace.settings.share', params: { namespaceId: namespace.id } }"
|
||||
icon="share-alt"
|
||||
>
|
||||
{{ $t('menu.share') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.create', params: { id: namespace.id } }"
|
||||
:to="{ name: 'list.create', params: { namespaceId: namespace.id } }"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('menu.newList') }}
|
||||
|
@ -34,6 +34,7 @@
|
|||
{{ $t('menu.archive') }}
|
||||
</dropdown-item>
|
||||
<task-subscription
|
||||
v-if="subscription"
|
||||
class="dropdown-item has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="namespace"
|
||||
|
|
|
@ -264,4 +264,6 @@ export default {
|
|||
.sharables-list:not(.card-content) {
|
||||
overflow-y: auto
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -365,3 +365,7 @@ export default {
|
|||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<router-link
|
||||
class="mt-2 has-text-centered is-block"
|
||||
:to="{name: 'task.detail', params: {id: taskEditTask.id}}"
|
||||
:to="taskDetailRoute"
|
||||
>
|
||||
{{ $t('task.openDetail') }}
|
||||
</router-link>
|
||||
|
@ -97,6 +97,15 @@ export default {
|
|||
taskEditTask: TaskModel,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
taskDetailRoute() {
|
||||
return {
|
||||
name: 'task.detail',
|
||||
params: { id: this.taskEditTask.id },
|
||||
state: { backdropView: this.$router.currentRoute.value.fullPath },
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ColorPicker,
|
||||
Reminders,
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
import TaskCollectionService from '@/services/taskCollection'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||
export const getDefaultParams = () => ({
|
||||
sort_by: ['position', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
})
|
||||
|
||||
/**
|
||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
taskCollectionService: new TaskCollectionService(),
|
||||
tasks: [],
|
||||
|
||||
currentPage: 0,
|
||||
|
||||
loadedList: null,
|
||||
|
||||
searchTerm: '',
|
||||
|
||||
showTaskFilter: false,
|
||||
params: {...getDefaultParams()},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// Only listen for query path changes
|
||||
'$route.query': {
|
||||
handler: 'loadTasksForPage',
|
||||
immediate: true,
|
||||
},
|
||||
'$route.path': 'loadTasksOnSavedFilter',
|
||||
},
|
||||
methods: {
|
||||
async loadTasks(
|
||||
page,
|
||||
search = '',
|
||||
params = null,
|
||||
forceLoading = false,
|
||||
) {
|
||||
// Because this function is triggered every time on topNavigation, we're putting a condition here to only load it when we actually want to show tasks
|
||||
// FIXME: This is a bit hacky -> Cleanup.
|
||||
if (
|
||||
this.$route.name !== 'list.list' &&
|
||||
this.$route.name !== 'list.table' &&
|
||||
!forceLoading
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (params === null) {
|
||||
params = this.params
|
||||
}
|
||||
|
||||
if (search !== '') {
|
||||
params.s = search
|
||||
}
|
||||
|
||||
const list = {listId: parseInt(this.$route.params.listId)}
|
||||
|
||||
const currentList = {
|
||||
id: list.listId,
|
||||
params,
|
||||
search,
|
||||
page,
|
||||
}
|
||||
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList) && !forceLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
this.tasks = []
|
||||
this.tasks = await this.taskCollectionService.getAll(list, params, page)
|
||||
this.currentPage = page
|
||||
this.loadedList = JSON.parse(JSON.stringify(currentList))
|
||||
},
|
||||
|
||||
loadTasksForPage(e) {
|
||||
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
|
||||
let page = Number(e.page)
|
||||
if (typeof e.page === 'undefined') {
|
||||
page = 1
|
||||
}
|
||||
let search = e.search
|
||||
if (typeof e.search === 'undefined') {
|
||||
search = ''
|
||||
}
|
||||
this.initTasks(page, search)
|
||||
},
|
||||
loadTasksOnSavedFilter() {
|
||||
if (typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
|
||||
this.loadTasks(1, '', null, true)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
|
@ -34,7 +34,7 @@
|
|||
>
|
||||
<div class="filename">{{ a.file.name }}</div>
|
||||
<div class="info">
|
||||
<p class="collapses">
|
||||
<p class="attachment-info-meta">
|
||||
<i18n-t keypath="task.attachment.createdBy">
|
||||
<span v-tooltip="formatDate(a.created)">
|
||||
{{ formatDateSince(a.created) }}
|
||||
|
@ -289,21 +289,6 @@ export default {
|
|||
content: '·';
|
||||
padding: 0 .25rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
&.collapses {
|
||||
flex-direction: column;
|
||||
|
||||
> span:not(:last-child):after,
|
||||
> a:not(:last-child):after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user .username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -341,6 +326,10 @@ export default {
|
|||
height: auto;
|
||||
text-shadow: var(--shadow-md);
|
||||
animation: bounce 2s infinite;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
|
@ -357,6 +346,35 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.attachment-info-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.user) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $mobile) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
:deep(.user) {
|
||||
margin: .5rem 0;
|
||||
}
|
||||
|
||||
> span:not(:last-child):after,
|
||||
> a:not(:last-child):after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user .username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
from,
|
||||
20%,
|
||||
|
@ -382,4 +400,6 @@ export default {
|
|||
transform: translate3d(0, -4px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -162,7 +162,7 @@ import {mapState} from 'vuex'
|
|||
export default {
|
||||
name: 'comments',
|
||||
components: {
|
||||
editor: AsyncEditor,
|
||||
Editor: AsyncEditor,
|
||||
},
|
||||
props: {
|
||||
taskId: {
|
||||
|
@ -339,4 +339,6 @@ export default {
|
|||
.media-content {
|
||||
width: calc(100% - 48px - 2rem);
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<td v-tooltip="+date === 0 ? '' : formatDate(date)">
|
||||
<time :datetime="date ? formatISO(date) : null">
|
||||
{{ +date === 0 ? '-' : formatDateSince(date) }}
|
||||
</time>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ import {mapState} from 'vuex'
|
|||
export default {
|
||||
name: 'description',
|
||||
components: {
|
||||
editor: AsyncEditor,
|
||||
Editor: AsyncEditor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -19,19 +19,19 @@
|
|||
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
|
||||
class="tag">
|
||||
<span>{{ props.item.title }}</span>
|
||||
<a @click="removeLabel(props.item)" class="delete is-small"></a>
|
||||
<button type="button" v-cy="'taskDetail.removeLabel'" @click="removeLabel(props.item)" class="delete is-small" />
|
||||
</span>
|
||||
</template>
|
||||
<template #searchResult="props">
|
||||
<span
|
||||
v-if="typeof props.option === 'string'"
|
||||
class="tag">
|
||||
class="tag search-result">
|
||||
<span>{{ props.option }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:style="{'background': props.option.hexColor, 'color': props.option.textColor}"
|
||||
class="tag">
|
||||
class="tag search-result">
|
||||
<span>{{ props.option.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
@ -114,7 +114,10 @@ export default {
|
|||
},
|
||||
|
||||
async removeLabel(label) {
|
||||
const removeFromState = () => {
|
||||
if (!this.taskId === 0) {
|
||||
await this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
|
||||
}
|
||||
|
||||
for (const l in this.labels) {
|
||||
if (this.labels[l].id === label.id) {
|
||||
this.labels.splice(l, 1)
|
||||
|
@ -122,15 +125,6 @@ export default {
|
|||
}
|
||||
this.$emit('update:modelValue', this.labels)
|
||||
this.$emit('change', this.labels)
|
||||
}
|
||||
|
||||
if (this.taskId === 0) {
|
||||
removeFromState()
|
||||
return
|
||||
}
|
||||
|
||||
await this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
|
||||
removeFromState()
|
||||
this.$message.success({message: this.$t('task.label.removeSuccess')})
|
||||
},
|
||||
|
||||
|
@ -152,6 +146,18 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.tag {
|
||||
margin: .5rem 0 0 .5rem;
|
||||
margin: .25rem !important;
|
||||
}
|
||||
|
||||
.tag.search-result {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.input-wrapper) {
|
||||
padding: .25rem !important;
|
||||
}
|
||||
|
||||
:deep(input.input) {
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
'has-light-text': !colorIsDark(task.hexColor) && task.hexColor !== `#${task.defaultColor}` && task.hexColor !== task.defaultColor,
|
||||
}"
|
||||
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
|
||||
@click.exact="openTaskDetail()"
|
||||
@click.ctrl="() => toggleTaskDone(task)"
|
||||
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
|
||||
@click.meta="() => toggleTaskDone(task)"
|
||||
>
|
||||
<span class="task-id">
|
||||
|
@ -28,9 +28,9 @@
|
|||
<span class="icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
</span>
|
||||
<span>
|
||||
<time :datetime="formatISO(task.dueDate)">
|
||||
{{ formatDateSince(task.dueDate) }}
|
||||
</span>
|
||||
</time>
|
||||
</span>
|
||||
<h3>{{ task.title }}</h3>
|
||||
<progress
|
||||
|
@ -115,6 +115,13 @@ export default {
|
|||
this.loadingInternal = false
|
||||
}
|
||||
},
|
||||
openTaskDetail() {
|
||||
this.$router.push({
|
||||
name: 'task.detail',
|
||||
params: { id: this.task.id },
|
||||
state: { backdropView: this.$router.currentRoute.value.fullPath },
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -274,10 +274,11 @@ export default {
|
|||
return tasks
|
||||
.map(task => {
|
||||
// by doing this here once we can save a lot of duplicate calls in the template
|
||||
const listAndNamespace = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
|
||||
const {
|
||||
list,
|
||||
namespace,
|
||||
} = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
|
||||
} = listAndNamespace === null ? {list: null, namespace: null} : listAndNamespace
|
||||
|
||||
return {
|
||||
...task,
|
||||
|
@ -364,4 +365,6 @@ export default {
|
|||
:deep(.multiselect .search-results button) {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@include modal-transition();
|
||||
</style>
|
|
@ -8,7 +8,7 @@
|
|||
>
|
||||
</span>
|
||||
<router-link
|
||||
:to="{ name: taskDetailRoute, params: { id: task.id } }"
|
||||
:to="taskDetailRoute"
|
||||
:class="{ 'done': task.done}"
|
||||
class="tasktext">
|
||||
<span>
|
||||
|
@ -39,14 +39,17 @@
|
|||
:user="a"
|
||||
v-for="(a, i) in task.assignees"
|
||||
/>
|
||||
<i
|
||||
<time
|
||||
:datetime="formatISO(task.dueDate)"
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
class="is-italic"
|
||||
@click.prevent.stop="showDefer = !showDefer"
|
||||
v-if="+new Date(task.dueDate) > 0"
|
||||
v-tooltip="formatDate(task.dueDate)"
|
||||
:aria-expanded="showDefer ? 'true' : 'false'"
|
||||
>
|
||||
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
|
||||
</i>
|
||||
</time>
|
||||
<transition name="fade">
|
||||
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
||||
</transition>
|
||||
|
@ -126,10 +129,6 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
taskDetailRoute: {
|
||||
type: String,
|
||||
default: 'task.list.detail',
|
||||
},
|
||||
showList: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -167,6 +166,14 @@ export default {
|
|||
title: '',
|
||||
} : this.$store.state.currentList
|
||||
},
|
||||
taskDetailRoute() {
|
||||
return {
|
||||
name: 'task.detail',
|
||||
params: { id: this.task.id },
|
||||
// TODO: re-enable opening task detail in modal
|
||||
// state: { backdropView: this.$router.currentRoute.value.fullPath },
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async markAsDone(checked) {
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
<template>
|
||||
<a @click="$emit('click')">
|
||||
<BaseButton>
|
||||
<icon icon="sort-up" v-if="order === 'asc'"/>
|
||||
<icon icon="sort-up" rotation="180" v-else-if="order === 'desc'"/>
|
||||
<icon icon="sort-up" v-else-if="order === 'desc'" rotation="180"/>
|
||||
<icon icon="sort" v-else/>
|
||||
</a>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'sort',
|
||||
props: {
|
||||
<script setup lang="ts">
|
||||
import {PropType} from 'vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
type Order = 'asc' | 'desc' | 'none'
|
||||
|
||||
defineProps({
|
||||
order: {
|
||||
type: String,
|
||||
type: String as PropType<Order>,
|
||||
default: 'none',
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
111
src/composables/taskList.js
Normal file
111
src/composables/taskList.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { ref, shallowReactive, watch, computed } from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||
export const getDefaultParams = () => ({
|
||||
sort_by: ['position', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
})
|
||||
|
||||
const SORT_BY_DEFAULT = {
|
||||
id: 'desc',
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||
*/
|
||||
export function useTaskList(listId) {
|
||||
const params = ref({...getDefaultParams()})
|
||||
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
|
||||
const sortBy = ref({ ...SORT_BY_DEFAULT })
|
||||
|
||||
|
||||
// This makes sure an id sort order is always sorted last.
|
||||
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
|
||||
// precedence over everything else, making any other sort columns pretty useless.
|
||||
function formatSortOrder(params) {
|
||||
let hasIdFilter = false
|
||||
const sortKeys = Object.keys(sortBy.value)
|
||||
for (const s of sortKeys) {
|
||||
if (s === 'id') {
|
||||
sortKeys.splice(s, 1)
|
||||
hasIdFilter = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (hasIdFilter) {
|
||||
sortKeys.push('id')
|
||||
}
|
||||
params.sort_by = sortKeys
|
||||
params.order_by = sortKeys.map(s => sortBy.value[s])
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
const getAllTasksParams = computed(() => {
|
||||
let loadParams = {...params.value}
|
||||
|
||||
if (search.value !== '') {
|
||||
loadParams.s = search.value
|
||||
}
|
||||
|
||||
loadParams = formatSortOrder(loadParams)
|
||||
|
||||
return [
|
||||
{listId: listId.value},
|
||||
loadParams,
|
||||
page.value || 1,
|
||||
]
|
||||
})
|
||||
|
||||
const taskCollectionService = shallowReactive(new TaskCollectionService())
|
||||
const loading = computed(() => taskCollectionService.loading)
|
||||
const totalPages = computed(() => taskCollectionService.totalPages)
|
||||
|
||||
const tasks = ref([])
|
||||
async function loadTasks() {
|
||||
tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value)
|
||||
return tasks.value
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
watch(() => route.query, (query) => {
|
||||
const { page: pageQueryValue, search: searchQuery } = query
|
||||
if (searchQuery !== undefined) {
|
||||
search.value = searchQuery
|
||||
}
|
||||
if (pageQueryValue !== undefined) {
|
||||
page.value = parseInt(pageQueryValue)
|
||||
}
|
||||
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
// Only listen for query path changes
|
||||
watch(() => JSON.stringify(getAllTasksParams.value), (newParams, oldParams) => {
|
||||
if (oldParams === newParams) {
|
||||
return
|
||||
}
|
||||
|
||||
loadTasks()
|
||||
}, { immediate: true })
|
||||
|
||||
return {
|
||||
tasks,
|
||||
loading,
|
||||
totalPages,
|
||||
currentPage: page,
|
||||
loadTasks,
|
||||
searchTerm: search,
|
||||
params,
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import {computed, watch, readonly} from 'vue'
|
||||
import {useStorage, createSharedComposable, ColorSchema, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
|
||||
import {useStorage, createSharedComposable, BasicColorSchema, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
|
||||
|
||||
const STORAGE_KEY = 'color-scheme'
|
||||
|
||||
const DEFAULT_COLOR_SCHEME_SETTING: ColorSchema = 'light'
|
||||
const DEFAULT_COLOR_SCHEME_SETTING: BasicColorSchema = 'light'
|
||||
|
||||
const CLASS_DARK = 'dark'
|
||||
const CLASS_LIGHT = 'light'
|
||||
|
@ -16,7 +16,7 @@ const CLASS_LIGHT = 'light'
|
|||
// - value is synced via `createSharedComposable`
|
||||
// https://github.com/vueuse/vueuse/blob/main/packages/core/useDark/index.ts
|
||||
export const useColorScheme = createSharedComposable(() => {
|
||||
const store = useStorage<ColorSchema>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
|
||||
const store = useStorage<BasicColorSchema>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
|
||||
|
||||
const preferredColorScheme = usePreferredColorScheme()
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { computed, watchEffect } from 'vue'
|
||||
import { setTitle } from '@/helpers/setTitle'
|
||||
|
||||
import { ComputedGetter, ComputedRef } from '@vue/reactivity'
|
||||
import { ComputedGetter } from '@vue/reactivity'
|
||||
|
||||
export function useTitle<T>(titleGetter: ComputedGetter<T>) : ComputedRef<T> {
|
||||
export function useTitle(titleGetter: ComputedGetter<string>) {
|
||||
const titleRef = computed(titleGetter)
|
||||
|
||||
watchEffect(() => setTitle(titleRef.value))
|
||||
|
|
|
@ -53,6 +53,7 @@ export async function refreshToken(persist: boolean): Promise<AxiosResponse> {
|
|||
return response
|
||||
|
||||
} catch(e) {
|
||||
// @ts-ignore
|
||||
throw new Error('Error renewing token: ', { cause: e })
|
||||
}
|
||||
}
|
||||
|
|
6
src/helpers/isEmail.ts
Normal file
6
src/helpers/isEmail.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export function isEmail(email: string): Boolean {
|
||||
const format = /^.+@.+$/
|
||||
const match = email.match(format)
|
||||
|
||||
return match === null ? false : match.length > 0
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
// Save the current list view to local storage
|
||||
// We use local storage and not vuex here to make it persistent across reloads.
|
||||
export const saveListView = (listId, routeName) => {
|
||||
if (routeName.includes('settings.')) {
|
||||
return
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export function setTitle(title) {
|
||||
export function setTitle(title : undefined | string) {
|
||||
document.title = (typeof title === 'undefined' || title === '')
|
||||
? 'Vikunja'
|
||||
: `${title} | Vikunja`
|
|
@ -1,5 +1,5 @@
|
|||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {format, formatDistanceToNow} from 'date-fns'
|
||||
import {format, formatDistanceToNow, formatISO as formatISOfns} from 'date-fns'
|
||||
import {enGB, de, fr, ru} from 'date-fns/locale'
|
||||
|
||||
import {i18n} from '@/i18n'
|
||||
|
@ -44,3 +44,7 @@ export const formatDateSince = (date) => {
|
|||
addSuffix: true,
|
||||
})
|
||||
}
|
||||
|
||||
export function formatISO(date) {
|
||||
return date ? formatISOfns(date) : ''
|
||||
}
|
||||
|
|
|
@ -288,7 +288,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
|
|||
}
|
||||
|
||||
const getDayFromText = (text: string) => {
|
||||
const matcher = /(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)/ig
|
||||
const matcher = /($| )(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)($| )/ig
|
||||
const results = matcher.exec(text)
|
||||
if (results === null) {
|
||||
return {
|
||||
|
@ -302,16 +302,16 @@ const getDayFromText = (text: string) => {
|
|||
const day = parseInt(results[0])
|
||||
date.setDate(day)
|
||||
|
||||
// If the parsed day is the 31st but the next month only has 30 days, setting the day to 31 will "overflow" the
|
||||
// date to the next month, but the first.
|
||||
// If the parsed day is the 31st (or 29+ and the next month is february) but the next month only has 30 days,
|
||||
// setting the day to 31 will "overflow" the date to the next month, but the first.
|
||||
// This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after
|
||||
// setting it for the first time and set it again if it isn't - that would mean the month overflowed.
|
||||
if (day === 31 && date.getDate() !== day) {
|
||||
date.setDate(day)
|
||||
while (date < now) {
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
}
|
||||
|
||||
if (date < now) {
|
||||
date.setMonth(date.getMonth() + 1)
|
||||
if (date.getDate() !== day) {
|
||||
date.setDate(day)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "Komentář k úkolu neexistuje.",
|
||||
"4016": "Neplatné pole úkolu.",
|
||||
"4017": "Neplatný komparátor filtru úkolů.",
|
||||
"4018": "Neplatný koncatinátor filtru úkolů.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Neplatná hodnota filtru úkolů.",
|
||||
"5001": "Prostor neexistuje.",
|
||||
"5003": "Nemáte přístup ke zvolenému prostoru.",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Zuletzt angesehen",
|
||||
"list": {
|
||||
"newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:",
|
||||
"new": "New list",
|
||||
"new": "Neue Liste",
|
||||
"importText": "Oder importiere deine Listen und Aufgaben aus anderen Diensten in Vikunja:",
|
||||
"import": "Deine Daten in Vikunja importieren"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen",
|
||||
"shared": "Geteilte Listen",
|
||||
"create": {
|
||||
"header": "New list",
|
||||
"header": "Neue Liste",
|
||||
"titlePlaceholder": "Der Titel der Liste steht hier…",
|
||||
"addTitleRequired": "Bitte gebe einen Namen an.",
|
||||
"createdSuccess": "Die Liste wurde erfolgreich erstellt.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namespaces",
|
||||
"search": "Beginne zu schreiben, um einen Namespace zu suchen…",
|
||||
"create": {
|
||||
"title": "New namespace",
|
||||
"title": "Neuer Namespace",
|
||||
"titleRequired": "Bitte gebe einen Titel an.",
|
||||
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste zu einem Namespace.",
|
||||
"tooltip": "Was ist ein Namespace?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Erinnerungs-Datumsbereich"
|
||||
},
|
||||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"title": "Neuer gespeicherter Filter",
|
||||
"description": "Ein gespeicherter Filter ist eine virtuelle Liste, die bei jedem Zugriff aus einem Satz von Filtern errechnet wird. Einmal erstellt, erscheint diese in einem speziellen Namespace.",
|
||||
"action": "Neuen gespeicherten Filter erstellen"
|
||||
},
|
||||
|
@ -545,7 +545,7 @@
|
|||
"chooseStartDate": "Klicke hier, um ein Startdatum zu setzen",
|
||||
"chooseEndDate": "Klicke hier, um ein Enddatum zu setzen",
|
||||
"move": "Aufgabe in eine andere Liste verschieben",
|
||||
"done": "Mark task done!",
|
||||
"done": "Als erledigt markieren!",
|
||||
"undone": "Als nicht erledigt markieren",
|
||||
"created": "Erstellt {0} von {1}",
|
||||
"updated": "Aktualisiert {0}",
|
||||
|
@ -781,7 +781,7 @@
|
|||
"then": "dann",
|
||||
"task": {
|
||||
"title": "Aufgabenseite",
|
||||
"done": "Done",
|
||||
"done": "Fertig",
|
||||
"assign": "Benutzer:in zuweisen",
|
||||
"labels": "Dieser Aufgabe ein Label hinzufügen",
|
||||
"dueDate": "Ändere das Fälligkeitsdatum dieser Aufgabe",
|
||||
|
@ -899,7 +899,7 @@
|
|||
"4015": "Dieser Aufgabenkommentar existiert nicht.",
|
||||
"4016": "Ungültiges Aufgabenfeld.",
|
||||
"4017": "Ungültiger Aufgabenfilter (Vergleichskriterium).",
|
||||
"4018": "Ungültiger Aufgabenfilter (Kombination).",
|
||||
"4018": "Ungültige Verkettung von Aufgabenfiltern.",
|
||||
"4019": "Ungültiger Aufgabenfilter (Wert).",
|
||||
"5001": "Dieser Namespace existiert nicht.",
|
||||
"5003": "Du hast keinen Zugriff auf den Namespace.",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Zletscht ahglueget",
|
||||
"list": {
|
||||
"newText": "Du chasch e Liste für dini neue Uufgabe erstelle:",
|
||||
"new": "New list",
|
||||
"new": "Neue Liste",
|
||||
"importText": "Oder importier dini Liste und Uufgabe us anderne Dienst nach Vikunja:",
|
||||
"import": "Dini Date in Vikunja importiere"
|
||||
}
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Druck uf Enter um die Liste uuszwähle",
|
||||
"shared": "Teilti Liste",
|
||||
"create": {
|
||||
"header": "New list",
|
||||
"header": "Neue Liste",
|
||||
"titlePlaceholder": "Listetitl da ahgeh…",
|
||||
"addTitleRequired": "Bitte gib en Titl ah.",
|
||||
"createdSuccess": "Liste erfolgriich erstellt.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"namespaces": "Namensrüüm",
|
||||
"search": "Schriib, um nachemne Namensruum z'sueche…",
|
||||
"create": {
|
||||
"title": "New namespace",
|
||||
"title": "Neuer Namespace",
|
||||
"titleRequired": "Bitte gib en Titl ah.",
|
||||
"explanation": "En Namensruum isch e Gruppe vo Liste, wo du chasch zur Organisation benutze. Tatsächlich sind alli Listene emne Namensruum zuegwise.",
|
||||
"tooltip": "Was isch en Namensruum?",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"reminderRange": "Errinnerigs Datumbereich"
|
||||
},
|
||||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"title": "Neuer gespeicherter Filter",
|
||||
"description": "En gspeicherete Filter isch e virtuelli Liste, welche vomene Satz a Filter zemmegsetzt wird, sobald me uf sie zuegriift. Wenn sie mal erstellt worde isch, erhaltet si ihren eigene Namensruum.",
|
||||
"action": "Neue gspeicherete Filter erstelle"
|
||||
},
|
||||
|
@ -545,7 +545,7 @@
|
|||
"chooseStartDate": "Druck dah, um es Startdatum z'setze",
|
||||
"chooseEndDate": "Druck da, um es Enddatum z'setze",
|
||||
"move": "Schieb die Uufgab in e anderi Liste",
|
||||
"done": "Mark task done!",
|
||||
"done": "Als erledigt markieren!",
|
||||
"undone": "Als unerledigt markierä",
|
||||
"created": "Erstellt am {0} vo {1}",
|
||||
"updated": "{0} g'updatet",
|
||||
|
@ -781,7 +781,7 @@
|
|||
"then": "dann",
|
||||
"task": {
|
||||
"title": "Uufgabesiite",
|
||||
"done": "Done",
|
||||
"done": "Fertig",
|
||||
"assign": "Benutzer:in zuweisen",
|
||||
"labels": "Labels ennere Uufgab hinzuefüege",
|
||||
"dueDate": "S'Fälligkeitsdatum für die Uufgab ändere",
|
||||
|
@ -899,7 +899,7 @@
|
|||
"4015": "De Uufgabe Kommentar giz nid.",
|
||||
"4016": "Ungültigs Uufgabefeld.",
|
||||
"4017": "Ungültige Uufgabefilter vergliich.",
|
||||
"4018": "Ungültige Uufgabefilter Zemmezug.",
|
||||
"4018": "Ungültige Verkettung von Aufgabenfiltern.",
|
||||
"4019": "Ungültigi Uufgabe Filter Wert.",
|
||||
"5001": "De Namensruum existiert nid.",
|
||||
"5003": "Du hesch kei Zuegriff zu dem Namensruum.",
|
||||
|
|
|
@ -31,10 +31,9 @@
|
|||
"username": "Username",
|
||||
"usernameEmail": "Username Or Email Address",
|
||||
"usernamePlaceholder": "e.g. frederick",
|
||||
"email": "E-mail address",
|
||||
"email": "Email address",
|
||||
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
|
||||
"password": "Password",
|
||||
"passwordRepeat": "Retype your password",
|
||||
"passwordPlaceholder": "e.g. •••••••••••",
|
||||
"forgotPassword": "Forgot your password?",
|
||||
"resetPassword": "Reset your password",
|
||||
|
@ -45,12 +44,19 @@
|
|||
"totpTitle": "Two Factor Authentication Code",
|
||||
"totpPlaceholder": "e.g. 123456",
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"createAccount": "Create account",
|
||||
"loginWith": "Log in with {provider}",
|
||||
"authenticating": "Authenticating…",
|
||||
"openIdStateError": "State does not match, refusing to continue!",
|
||||
"openIdGeneralError": "An error occured while authenticating against the third party.",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
"alreadyHaveAnAccount": "Already have an account?"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
|
@ -61,7 +67,7 @@
|
|||
"currentPasswordPlaceholder": "Your current password",
|
||||
"passwordsDontMatch": "The new password and its confirmation don't match.",
|
||||
"passwordUpdateSuccess": "The password was successfully updated.",
|
||||
"updateEmailTitle": "Update Your E-Mail Address",
|
||||
"updateEmailTitle": "Update Your Email Address",
|
||||
"updateEmailNew": "New Email Address",
|
||||
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
|
||||
"general": {
|
||||
|
@ -904,7 +910,7 @@
|
|||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
|
|
|
@ -116,12 +116,12 @@
|
|||
"vikunja": "Vikunja"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Color Scheme",
|
||||
"setSuccess": "Saved change of color scheme to {colorScheme}",
|
||||
"title": "Jeu de couleurs",
|
||||
"setSuccess": "Changement du jeu de couleurs enregistré vers {colorScheme}",
|
||||
"colorScheme": {
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"dark": "Dark"
|
||||
"light": "Clair",
|
||||
"system": "Système",
|
||||
"dark": "Sombre"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -475,7 +475,7 @@
|
|||
"download": "Télécharger",
|
||||
"showMenu": "Afficher le menu",
|
||||
"hideMenu": "Masquer le menu",
|
||||
"forExample": "For example:",
|
||||
"forExample": "Par exemple :",
|
||||
"welcomeBack": "Welcome Back!"
|
||||
},
|
||||
"input": {
|
||||
|
@ -561,7 +561,7 @@
|
|||
"text2": "Ceci supprimera également toutes les pièces jointes, les rappels et les relations associés à cette tâche et ne pourra pas être annulé !"
|
||||
},
|
||||
"actions": {
|
||||
"assign": "Assign to a user",
|
||||
"assign": "Attribuer à un utilisateur",
|
||||
"label": "Ajouter des étiquettes",
|
||||
"priority": "Définir la priorité",
|
||||
"dueDate": "Définir l’échéance",
|
||||
|
@ -726,8 +726,8 @@
|
|||
"dateCurrentYear": "utilisera l’année en cours",
|
||||
"dateNth": "utilisera le {day}e du mois en cours",
|
||||
"dateTime": "Combinez n’importe lequel des formats de date avec « {time} » (ou {timePM}) pour définir une heure.",
|
||||
"repeats": "Repeating tasks",
|
||||
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
|
||||
"repeats": "Tâches répétitives",
|
||||
"repeatsDescription": "Pour définir une tâche comme répétitive dans un intervalle, il suffit d'ajouter « {suffix} » au texte de la tâche. Le montant doit être un nombre et peut être omis pour utiliser uniquement le type (voir exemples)."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
|
@ -782,7 +782,7 @@
|
|||
"task": {
|
||||
"title": "Page de tâche",
|
||||
"done": "Done",
|
||||
"assign": "Assign to a user",
|
||||
"assign": "Attribuer à un utilisateur",
|
||||
"labels": "Ajouter des étiquettes à cette tâche",
|
||||
"dueDate": "Modifier la date d’échéance de cette tâche",
|
||||
"attachment": "Ajouter une pièce jointe à cette tâche",
|
||||
|
@ -899,7 +899,7 @@
|
|||
"4015": "Le commentaire de la tâche n’existe pas.",
|
||||
"4016": "Champ de tâche invalide.",
|
||||
"4017": "Comparateur de filtre de tâche invalide.",
|
||||
"4018": "Concaténateur de filtre de tâche invalide.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Valeur de filtre de tâche invalide.",
|
||||
"5001": "L’espace de noms n’existe pas.",
|
||||
"5003": "Tu n’as pas accès à l’espace de noms indiqué.",
|
||||
|
@ -908,7 +908,7 @@
|
|||
"5010": "Cette équipe n’a pas accès à cet espace de noms.",
|
||||
"5011": "Cet·e utilisateur·rice a déjà accès à cet espace de noms.",
|
||||
"5012": "L’espace de noms est archivé et ne peut donc être consulté qu’en lecture seule.",
|
||||
"6001": "The team name cannot be empty.",
|
||||
"6001": "Le nom de l'équipe ne peut pas être vide.",
|
||||
"6002": "L’équipe n’existe pas.",
|
||||
"6004": "L’équipe a déjà accès à cet espace de noms ou à cette liste.",
|
||||
"6005": "L’utilisateur·rice est déjà membre de cette équipe.",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"lastViewed": "Ultima visualizzazione",
|
||||
"list": {
|
||||
"newText": "È possibile creare una nuova lista per le nuove attività:",
|
||||
"new": "New list",
|
||||
"new": "Nuova lista",
|
||||
"importText": "O importare le liste e le attività da altri servizi in Vikunja:",
|
||||
"import": "Importa i tuoi dati in Vikunja"
|
||||
}
|
||||
|
@ -17,14 +17,14 @@
|
|||
"text": "La pagina richiesta non esiste."
|
||||
},
|
||||
"ready": {
|
||||
"loading": "Vikunja is loading…",
|
||||
"errorOccured": "An error occured:",
|
||||
"checkApiUrl": "Please check if the api url is correct.",
|
||||
"noApiUrlConfigured": "No API url was configured. Please set one below:"
|
||||
"loading": "Vikunja sta caricando…",
|
||||
"errorOccured": "Si è verificato un errore:",
|
||||
"checkApiUrl": "Controlla se l'URL API è corretto.",
|
||||
"noApiUrlConfigured": "Nessun URL API configurato. Impostane uno qui sotto:"
|
||||
},
|
||||
"offline": {
|
||||
"title": "You are offline.",
|
||||
"text": "Please check your network connection and try again."
|
||||
"title": "Sei offline.",
|
||||
"text": "Controlla la connessione di rete e riprova."
|
||||
},
|
||||
"user": {
|
||||
"auth": {
|
||||
|
@ -36,7 +36,7 @@
|
|||
"password": "Password",
|
||||
"passwordRepeat": "Digita di nuovo la tua password",
|
||||
"passwordPlaceholder": "es. ••••••••••••",
|
||||
"forgotPassword": "Forgot your password?",
|
||||
"forgotPassword": "Password dimenticata?",
|
||||
"resetPassword": "Reimposta la tua password",
|
||||
"resetPasswordAction": "Inviami il link per reimpostare la password",
|
||||
"resetPasswordSuccess": "Controlla la tua casella di posta! Dovresti avere un'e-mail con le istruzioni su come reimpostare la password.",
|
||||
|
@ -48,7 +48,7 @@
|
|||
"register": "Registrati",
|
||||
"loginWith": "Accedi con {provider}",
|
||||
"authenticating": "Autenticazione…",
|
||||
"openIdStateError": "State does not match, refusing to continue!",
|
||||
"openIdStateError": "Stato non corrispondente, impossibile continuare!",
|
||||
"openIdGeneralError": "Si è verificato un errore durante l'autenticazione con terze parti.",
|
||||
"logout": "Esci"
|
||||
},
|
||||
|
@ -103,31 +103,31 @@
|
|||
"title": "Avatar",
|
||||
"initials": "Iniziali",
|
||||
"gravatar": "Gravatar",
|
||||
"marble": "Marble",
|
||||
"marble": "Marmo",
|
||||
"upload": "Carica",
|
||||
"uploadAvatar": "Carica Avatar",
|
||||
"statusUpdateSuccess": "Avatar status was updated successfully!",
|
||||
"statusUpdateSuccess": "Avatar aggiornato!",
|
||||
"setSuccess": "L'avatar è stato impostato con successo!"
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"title": "Quick Add Magic Mode",
|
||||
"title": "Modalità Aggiunta Rapida Magica",
|
||||
"disabled": "Disabilitato",
|
||||
"todoist": "Todoist",
|
||||
"vikunja": "Vikunja"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Color Scheme",
|
||||
"setSuccess": "Saved change of color scheme to {colorScheme}",
|
||||
"title": "Tema",
|
||||
"setSuccess": "Tema cambiato in {colorScheme}",
|
||||
"colorScheme": {
|
||||
"light": "Light",
|
||||
"system": "System",
|
||||
"dark": "Dark"
|
||||
"light": "Chiaro",
|
||||
"system": "Sistema",
|
||||
"dark": "Scuro"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"title": "Elimina il tuo Account Vikunja",
|
||||
"text1": "La cancellazione del tuo account è permanente e non può essere annullata. Elimineremo tutti i tuoi namespace, liste, attività e tutto ciò che è ad esso associato.",
|
||||
"text2": "Per continuare, inserisci la tua password. Riceverai un'e-mail con ulteriori istruzioni.",
|
||||
"confirm": "Elimina il mio profilo",
|
||||
"requestSuccess": "Richiesta riuscita. Riceverai un'e-mail con ulteriori istruzioni.",
|
||||
|
@ -141,7 +141,7 @@
|
|||
},
|
||||
"export": {
|
||||
"title": "Esporta i tuoi dati Vikunja",
|
||||
"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": "Puoi richiedere una copia di tutti i tuoi dati all'interno di Vikunja. Questo include i Namespace, le Liste, le Attività e tutto ciò che è loro associato. È possibile importare questi dati in qualsiasi istanza Vikunja attraverso la funzione di migrazione.",
|
||||
"descriptionPasswordRequired": "Inserisci la tua password per procedere:",
|
||||
"request": "Richiedi una copia dei miei dati Vikunja",
|
||||
"success": "Hai richiesto con successo i tuoi dati Vikunja! Ti invieremo un'e-mail una volta che saranno pronti da scaricare.",
|
||||
|
@ -157,7 +157,7 @@
|
|||
"searchSelect": "Fare clic o premere invio per selezionare questa lista",
|
||||
"shared": "Liste Condivise",
|
||||
"create": {
|
||||
"header": "New list",
|
||||
"header": "Nuova lista",
|
||||
"titlePlaceholder": "Il titolo della lista va qui…",
|
||||
"addTitleRequired": "Specifica un titolo.",
|
||||
"createdSuccess": "La lista è stata creata correttamente.",
|
||||
|
@ -191,7 +191,7 @@
|
|||
"duplicate": {
|
||||
"title": "Duplica questa lista",
|
||||
"label": "Duplica",
|
||||
"text": "Select a namespace which should hold the duplicated list:",
|
||||
"text": "Seleziona un namespace che dovrebbe contenere l'elenco duplicato:",
|
||||
"success": "Lista duplicata."
|
||||
},
|
||||
"edit": {
|
||||
|
@ -279,23 +279,23 @@
|
|||
"title": "Kanban",
|
||||
"limit": "Limite: {limit}",
|
||||
"noLimit": "Non Impostato",
|
||||
"doneBucket": "Done bucket",
|
||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
||||
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
|
||||
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
|
||||
"deleteLast": "You cannot remove the last bucket.",
|
||||
"addTaskPlaceholder": "Enter the new task title…",
|
||||
"doneBucket": "Colonna attività completate",
|
||||
"doneBucketHint": "Tutte le attività spostate in questa colonna verranno automaticamente contrassegnate come completate.",
|
||||
"doneBucketHintExtended": "Tutte le attività spostate nella colonna attività completate saranno contrassegnate automaticamente come completate. Tutte le attività contrassegnate come completate altrove verranno anche spostate.",
|
||||
"doneBucketSavedSuccess": "Colonna attività completate salvata.",
|
||||
"deleteLast": "Impossibile eliminare l'ultima colonna.",
|
||||
"addTaskPlaceholder": "Inserisci il nuovo titolo dell'attività…",
|
||||
"addTask": "Aggiungi un'attività",
|
||||
"addAnotherTask": "Aggiungi un'altra attività",
|
||||
"addBucket": "Create a new bucket",
|
||||
"addBucketPlaceholder": "Enter the new bucket title…",
|
||||
"deleteHeaderBucket": "Delete the bucket",
|
||||
"deleteBucketText1": "Are you sure you want to delete this bucket?",
|
||||
"deleteBucketText2": "This will not delete any tasks but move them into the default bucket.",
|
||||
"deleteBucketSuccess": "The bucket has been deleted successfully.",
|
||||
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
|
||||
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
|
||||
"collapse": "Collapse this bucket"
|
||||
"addBucket": "Crea una nuova colonna",
|
||||
"addBucketPlaceholder": "Inserisci il titolo della nuova colonna…",
|
||||
"deleteHeaderBucket": "Elimina la colonna",
|
||||
"deleteBucketText1": "Confermi di voler eliminare questa colonna?",
|
||||
"deleteBucketText2": "Questo non eliminerà nessuna attività, ma la sposterà nel bucket predefinito.",
|
||||
"deleteBucketSuccess": "Colonna eliminata.",
|
||||
"bucketTitleSavedSuccess": "Titolo della colonna salvato.",
|
||||
"bucketLimitSavedSuccess": "Limite della colonna salvato.",
|
||||
"collapse": "Comprimi questa colonna"
|
||||
},
|
||||
"pseudo": {
|
||||
"favorites": {
|
||||
|
@ -304,52 +304,52 @@
|
|||
}
|
||||
},
|
||||
"namespace": {
|
||||
"title": "Namespaces & Lists",
|
||||
"title": "Namespace e Liste",
|
||||
"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.",
|
||||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"showArchived": "Mostra Archiviati",
|
||||
"noneAvailable": "Non hai alcun namespace in questo momento.",
|
||||
"unarchive": "De-Archivia",
|
||||
"archived": "Archiviato",
|
||||
"noLists": "Questo namespace non contiene alcuna lista.",
|
||||
"createList": "Crea una nuova lista in questo namespace.",
|
||||
"namespaces": "Namespace",
|
||||
"search": "Digita per cercare un 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 namepace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
"success": "The namespace was successfully created."
|
||||
"title": "Nuovo namespace",
|
||||
"titleRequired": "Specifica un titolo.",
|
||||
"explanation": "Un namespace è una raccolta di liste che puoi condividere e che puoi usare per organizzare le tue liste. Infatti, ogni lista appartiene a un namespace.",
|
||||
"tooltip": "Che cos'è un namespace?",
|
||||
"success": "Namespace creato."
|
||||
},
|
||||
"archive": {
|
||||
"titleArchive": "Archivia \"{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.",
|
||||
"success": "The namespace was successfully archived.",
|
||||
"description": "If a namespace is archived, you cannot create new lists or edit it."
|
||||
"titleUnarchive": "Disarchivia \"{namespace}\"",
|
||||
"archiveText": "Non sarà possibile modificare questo namespace o creare nuove liste fino a quando non verrà disarchiviato. Questo archivierà anche tutte le liste in questo namespace.",
|
||||
"unarchiveText": "Potrai creare nuove liste o modificarle.",
|
||||
"success": "Namespace creato.",
|
||||
"description": "Se un namespace è archiviato, non è possibile creare nuove liste o modificarlo."
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete \"{namespace}\"",
|
||||
"text1": "Are you sure you want to delete this namespace and all of its contents?",
|
||||
"title": "Elimina \"{namespace}\"",
|
||||
"text1": "Sei sicuro di voler rimuovere questo namespace e tutto il relativo contenuto?",
|
||||
"text2": "Questo include tutte le liste e le attività e NON PUÒ ESSERE RIPRISTINATO!",
|
||||
"success": "The namespace was successfully deleted."
|
||||
"success": "Namespace eliminato."
|
||||
},
|
||||
"edit": {
|
||||
"title": "Modifica \"{namespace}\"",
|
||||
"success": "The namespace was successfully updated."
|
||||
"success": "Namespace aggiornato."
|
||||
},
|
||||
"share": {
|
||||
"title": "Condividi \"{namespace}\""
|
||||
},
|
||||
"attributes": {
|
||||
"title": "Namespace Title",
|
||||
"titlePlaceholder": "The namespace title goes here…",
|
||||
"title": "Titolo del Namespace",
|
||||
"titlePlaceholder": "Il titolo del namespace va qui…",
|
||||
"description": "Descrizione",
|
||||
"descriptionPlaceholder": "The namespaces description goes here…",
|
||||
"descriptionPlaceholder": "La descrizione del namespace va qui…",
|
||||
"color": "Colore",
|
||||
"archived": "Is Archived",
|
||||
"isArchived": "This namespace is archived"
|
||||
"archived": "Archiviato",
|
||||
"isArchived": "Questo namespace è archiviato"
|
||||
},
|
||||
"pseudo": {
|
||||
"sharedLists": {
|
||||
|
@ -365,7 +365,7 @@
|
|||
},
|
||||
"filters": {
|
||||
"title": "Filtri",
|
||||
"clear": "Clear Filters",
|
||||
"clear": "Pulisci Filtri",
|
||||
"attributes": {
|
||||
"title": "Titolo",
|
||||
"titlePlaceholder": "Il titolo del filtro salvato va qui…",
|
||||
|
@ -374,17 +374,17 @@
|
|||
"includeNulls": "Includi attività che non hanno un valore impostato",
|
||||
"requireAll": "Tutti i filtri devono essere veri affinché l'attività venga mostrata",
|
||||
"showDoneTasks": "Mostra Attività Fatte",
|
||||
"sortAlphabetically": "Sort Alphabetically",
|
||||
"sortAlphabetically": "Ordine alfabetico",
|
||||
"enablePriority": "Abilita Filtro Per Priorità",
|
||||
"enablePercentDone": "Abilitare Filtro Per Percentuale Fatta",
|
||||
"dueDateRange": "Intervallo Data Di Scadenza",
|
||||
"startDateRange": "Intervallo Data Iniziale",
|
||||
"endDateRange": "Intervallo Data Finale",
|
||||
"reminderRange": "Reminder Date Range"
|
||||
"reminderRange": "Intervallo date dei promemoria"
|
||||
},
|
||||
"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.",
|
||||
"title": "Nuovo Filtro Salvato",
|
||||
"description": "Un filtro salvato è una lista virtuale che viene calcolata da un insieme di filtri di volta in volta. Una volta creato, apparirà in un namespace speciale.",
|
||||
"action": "Crea nuovo filtro salvato"
|
||||
},
|
||||
"delete": {
|
||||
|
@ -446,9 +446,9 @@
|
|||
},
|
||||
"navigation": {
|
||||
"overview": "Panoramica",
|
||||
"upcoming": "Upcoming",
|
||||
"upcoming": "Prossimamente",
|
||||
"settings": "Impostazioni",
|
||||
"imprint": "Imprint",
|
||||
"imprint": "Informazioni legali",
|
||||
"privacy": "Politica sulla Privacy"
|
||||
},
|
||||
"misc": {
|
||||
|
@ -464,19 +464,19 @@
|
|||
"searchPlaceholder": "Digita per cercare…",
|
||||
"previous": "Precedente",
|
||||
"next": "Successivo",
|
||||
"poweredBy": "Powered by Vikunja",
|
||||
"poweredBy": "Creato con Vikunja",
|
||||
"info": "Info",
|
||||
"create": "Create",
|
||||
"create": "Crea",
|
||||
"doit": "Fallo!",
|
||||
"saving": "Salvataggio…",
|
||||
"saved": "Salvato!",
|
||||
"default": "Predefinito",
|
||||
"close": "Chiudi",
|
||||
"download": "Scarica",
|
||||
"showMenu": "Show the menu",
|
||||
"hideMenu": "Hide the menu",
|
||||
"forExample": "For example:",
|
||||
"welcomeBack": "Welcome Back!"
|
||||
"showMenu": "Mostra il menu",
|
||||
"hideMenu": "Nascondi il menù",
|
||||
"forExample": "Ad esempio:",
|
||||
"welcomeBack": "Bentornato!"
|
||||
},
|
||||
"input": {
|
||||
"resetColor": "Ripristina Colore",
|
||||
|
@ -485,9 +485,9 @@
|
|||
"tomorrow": "Domani",
|
||||
"nextMonday": "Lunedì Prossimo",
|
||||
"thisWeekend": "Questo fine settimana",
|
||||
"laterThisWeek": "Later This Week",
|
||||
"laterThisWeek": "Alla fine di questa settimana",
|
||||
"nextWeek": "Prossima Settimana",
|
||||
"chooseDate": "Choose a date"
|
||||
"chooseDate": "Seleziona una data"
|
||||
},
|
||||
"editor": {
|
||||
"edit": "Modifica",
|
||||
|
@ -504,16 +504,16 @@
|
|||
"quote": "Citazione",
|
||||
"unorderedList": "Elenco puntato",
|
||||
"orderedList": "Elenco numerato",
|
||||
"cleanBlock": "Clean Block",
|
||||
"cleanBlock": "Pulisci Blocco",
|
||||
"link": "Link",
|
||||
"image": "Immagine",
|
||||
"table": "Tabella",
|
||||
"horizontalRule": "Horizontal Rule",
|
||||
"sideBySide": "Side By Side",
|
||||
"guide": "Guide"
|
||||
"horizontalRule": "Divisore Orizzontale",
|
||||
"sideBySide": "Affianca",
|
||||
"guide": "Guida"
|
||||
},
|
||||
"multiselect": {
|
||||
"createPlaceholder": "Create new",
|
||||
"createPlaceholder": "Crea nuovo",
|
||||
"selectPlaceholder": "Clicca o premere invio per selezionare"
|
||||
}
|
||||
},
|
||||
|
@ -533,19 +533,19 @@
|
|||
"titleDates": "Attività dal {from} al {to}",
|
||||
"noDates": "Mostra attività senza date",
|
||||
"current": "Attività attuali",
|
||||
"from": "Tasks from",
|
||||
"until": "until",
|
||||
"from": "Attività dal",
|
||||
"until": "fino al",
|
||||
"today": "Oggi",
|
||||
"nextWeek": "Settimana Prossima",
|
||||
"nextMonth": "Prossimo Mese",
|
||||
"noTasks": "Nothing to do — Have a nice day!"
|
||||
"noTasks": "Nessuna attività — Buona giornata!"
|
||||
},
|
||||
"detail": {
|
||||
"chooseDueDate": "Clicca qui per impostare una data di scadenza",
|
||||
"chooseStartDate": "Clicca qui per impostare una data di inizio",
|
||||
"chooseEndDate": "Clicca qui per impostare una data di fine",
|
||||
"move": "Sposta attività in un'altra lista",
|
||||
"done": "Mark task done!",
|
||||
"done": "Segna attività fatta!",
|
||||
"undone": "Segna come non completato",
|
||||
"created": "Creato {0} da {1}",
|
||||
"updated": "Aggiornato {0}",
|
||||
|
@ -554,21 +554,21 @@
|
|||
"deleteSuccess": "L'attività è stata eliminata con successo.",
|
||||
"belongsToList": "Questa attività appartiene alla lista '{list}'",
|
||||
"due": "Scadenza {at}",
|
||||
"closePopup": "Close popup",
|
||||
"closePopup": "Chiudi popup",
|
||||
"delete": {
|
||||
"header": "Elimina questa attività",
|
||||
"text1": "Sei sicuro di voler eliminare questa attività?",
|
||||
"text2": "Questo rimuoverà anche tutti gli allegati, i promemoria e le relazioni associati a questa attività e non può essere ripristinato!"
|
||||
},
|
||||
"actions": {
|
||||
"assign": "Assign to a user",
|
||||
"assign": "Assegna ad un utente",
|
||||
"label": "Aggiungi etichette",
|
||||
"priority": "Imposta Priorità",
|
||||
"dueDate": "Imposta data di scadenza",
|
||||
"startDate": "Imposta una data di inizio",
|
||||
"endDate": "Imposta una data di fine",
|
||||
"reminders": "Imposta promemoria",
|
||||
"repeatAfter": "Set a repeating interval",
|
||||
"repeatAfter": "Imposta ricorrenza",
|
||||
"percentDone": "Imposta Percentuale Completata",
|
||||
"attachments": "Aggiungi allegati",
|
||||
"relatedTasks": "Aggiungi attività collegate",
|
||||
|
@ -599,13 +599,13 @@
|
|||
"updated": "Aggiornato"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.",
|
||||
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.",
|
||||
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.",
|
||||
"subscribe": "Subscribe",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"subscribeSuccess": "You are now subscribed to this {entity}",
|
||||
"unsubscribeSuccess": "You are now unsubscribed to this {entity}"
|
||||
"subscribedThroughParent": "Non puoi annullare l'iscrizione qui perché sei iscritto a questo {entity} attraverso il suo {parent}.",
|
||||
"subscribed": "Sei attualmente iscritto a questo {entity} e riceverai notifiche per le modifiche.",
|
||||
"notSubscribed": "Non sei iscritto a questo {entity} e non riceverai notifiche per le modifiche.",
|
||||
"subscribe": "Iscriviti",
|
||||
"unsubscribe": "Disiscriviti",
|
||||
"subscribeSuccess": "Ti sei iscritto a questo {entity}",
|
||||
"unsubscribeSuccess": "Ti sei disiscritto a questo {entity}"
|
||||
},
|
||||
"attachment": {
|
||||
"title": "Allegati",
|
||||
|
@ -623,41 +623,41 @@
|
|||
"comment": {
|
||||
"title": "Commenti",
|
||||
"loading": "Caricamento commenti…",
|
||||
"edited": "edited {date}",
|
||||
"edited": "modificato il {date}",
|
||||
"creating": "Creazione del commento…",
|
||||
"placeholder": "Aggiungi un commento…",
|
||||
"comment": "Comment",
|
||||
"comment": "Commenta",
|
||||
"delete": "Elimina questo commento",
|
||||
"deleteText1": "Sei sicuro di voler eliminare questo commento?",
|
||||
"deleteText2": "Questa azione non può essere annullata!",
|
||||
"addedSuccess": "Il commento è stato aggiunto correttamente."
|
||||
},
|
||||
"deferDueDate": {
|
||||
"title": "Defer due date",
|
||||
"title": "Rinvia data di scadenza",
|
||||
"1day": "1 giorno",
|
||||
"3days": "3 giorni",
|
||||
"1week": "1 settimana"
|
||||
},
|
||||
"description": {
|
||||
"placeholder": "Click here to enter a description…",
|
||||
"empty": "No description available yet."
|
||||
"placeholder": "Clicca qui per inserire una descrizione…",
|
||||
"empty": "Nessuna descrizione."
|
||||
},
|
||||
"assignee": {
|
||||
"placeholder": "Type to assign a user…",
|
||||
"placeholder": "Digita per assegnare un utente…",
|
||||
"selectPlaceholder": "Assegna questo utente",
|
||||
"assignSuccess": "The user has been assigned successfully.",
|
||||
"unassignSuccess": "The user has been unassigned successfully."
|
||||
"assignSuccess": "Utente assegnato.",
|
||||
"unassignSuccess": "Utente disassegnato."
|
||||
},
|
||||
"label": {
|
||||
"placeholder": "Type to add a new label…",
|
||||
"createPlaceholder": "Add this as new label",
|
||||
"placeholder": "Digita per aggiungere una nuova etichetta…",
|
||||
"createPlaceholder": "Aggiungila come nuova etichetta",
|
||||
"addSuccess": "Etichetta aggiunta.",
|
||||
"createSuccess": "Etichetta creata.",
|
||||
"removeSuccess": "Etichetta eliminata.",
|
||||
"addCreateSuccess": "Etichetta creata e aggiunta."
|
||||
},
|
||||
"priority": {
|
||||
"unset": "Unset",
|
||||
"unset": "Azzera",
|
||||
"low": "Bassa",
|
||||
"medium": "Media",
|
||||
"high": "Alta",
|
||||
|
@ -665,38 +665,38 @@
|
|||
"doNow": "FARE ORA"
|
||||
},
|
||||
"relation": {
|
||||
"add": "Add a New Task Relation",
|
||||
"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.",
|
||||
"differentNamespace": "This task belongs to a different namespace.",
|
||||
"noneYet": "No task relations yet.",
|
||||
"delete": "Delete Task Relation",
|
||||
"deleteText1": "Are you sure you want to delete this task relation?",
|
||||
"add": "Aggiungi Attività Collegata",
|
||||
"new": "Nuova Attività Collegata",
|
||||
"searchPlaceholder": "Digita per cercare un'attività da aggiungere come collegata…",
|
||||
"createPlaceholder": "Aggiungi come attività collegata",
|
||||
"differentList": "Questa attività è di una lista diversa.",
|
||||
"differentNamespace": "Questa attività appartiene ad un namespace diverso.",
|
||||
"noneYet": "Nessuna attività collegata.",
|
||||
"delete": "Elimina Collegamento Attività",
|
||||
"deleteText1": "Confermi di voler eliminare questo collegamento attività?",
|
||||
"deleteText2": "Questa azione non può essere annullata!",
|
||||
"select": "Select a relation kind",
|
||||
"select": "Seleziona un tipo di collegamento",
|
||||
"kinds": {
|
||||
"subtask": "Subtask | Subtasks",
|
||||
"parenttask": "Parent Task | Parent Tasks",
|
||||
"related": "Related Task | Related Tasks",
|
||||
"subtask": "Sotto-attività | Sotto-attività",
|
||||
"parenttask": "Attività Principale | Attività Principale",
|
||||
"related": "Attività Correlata | Attività Correlata",
|
||||
"duplicateof": "Duplicato Di | Duplicati Di",
|
||||
"duplicates": "Duplicates | Duplicates",
|
||||
"blocking": "Blocking | Blocking",
|
||||
"blocked": "Blocked By | Blocked By",
|
||||
"precedes": "Precedes | Precedes",
|
||||
"follows": "Follows | Follows",
|
||||
"copiedfrom": "Copied From | Copied From",
|
||||
"copiedto": "Copied To | Copied To"
|
||||
"duplicates": "Duplicato | Duplicati",
|
||||
"blocking": "Bloccante | Bloccanti",
|
||||
"blocked": "Bloccato Da | Bloccati Da",
|
||||
"precedes": "Precede | Precede",
|
||||
"follows": "Segue | Segue",
|
||||
"copiedfrom": "Copiata Da | Copiate Da",
|
||||
"copiedto": "Copiata In | Copiate In"
|
||||
}
|
||||
},
|
||||
"repeat": {
|
||||
"everyDay": "Ogni Giorno",
|
||||
"everyWeek": "Ogni Settimana",
|
||||
"everyMonth": "Ogni Mese",
|
||||
"mode": "Repeat mode",
|
||||
"mode": "Modalità Ripetizione",
|
||||
"monthly": "Mensilmente",
|
||||
"fromCurrentDate": "From Current Date",
|
||||
"fromCurrentDate": "Dalla Data Attuale",
|
||||
"each": "Ogni",
|
||||
"specifyAmount": "Specifica una quantità…",
|
||||
"hours": "Ore",
|
||||
|
@ -706,32 +706,32 @@
|
|||
"years": "Anni"
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
"hint": "Puoi usare l'Aggiunta Rapida Magica",
|
||||
"what": "Cosa?",
|
||||
"title": "Quick Add Magic",
|
||||
"intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.",
|
||||
"title": "Aggiunta Rapida Magica",
|
||||
"intro": "Quando si crea un'attività, è possibile utilizzare parole chiave speciali per aggiungere direttamente attributi all'attività appena creata. Questo permette di aggiungere gli attributi comuni molto più velocemente.",
|
||||
"multiple": "Puoi usarlo più volte.",
|
||||
"label1": "To add a label, simply prefix the name of the label with {prefix}.",
|
||||
"label2": "Vikunja will first check if the label already exist and create it if not.",
|
||||
"label3": "To use spaces, simply add a \" around the label name.",
|
||||
"label4": "For example: {prefix}\"Label with spaces\".",
|
||||
"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.",
|
||||
"label1": "Per aggiungere un'etichetta, basta aggiungere il nome dell'etichetta preceduto da {prefix}.",
|
||||
"label2": "Vikunja controllerà prima se l'etichetta esiste già e nel caso la creerà.",
|
||||
"label3": "Per usare gli spazi, basta \" prima e dopo del nome dell'etichetta.",
|
||||
"label4": "Per esempio: {prefix}\"Etichetta con spazi\".",
|
||||
"priority1": "Per impostare la priorità di un'attività, aggiungi un numero 1-5, preceduto da {prefix}.",
|
||||
"priority2": "Più alto è il numero, più alta è la priorità.",
|
||||
"assignees": "Per assegnare direttamente l'attività a un utente, aggiungere il suo nome utente preceduto da {prefix} all'attività.",
|
||||
"list1": "Per impostare una lista di appartenenza all'attività, inserisci il suo nome prefisso con {prefix}.",
|
||||
"list2": "Ciò restituirà un errore se la lista non esiste.",
|
||||
"dateAndTime": "Data e ora",
|
||||
"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",
|
||||
"dateCurrentYear": "will use the current year",
|
||||
"dateNth": "will use the {day}th of the current month",
|
||||
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
|
||||
"repeats": "Repeating tasks",
|
||||
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)."
|
||||
"date": "Qualsiasi data verrà utilizzata come data di scadenza della nuova attività. È possibile utilizzare le date in uno qualsiasi di questi formati:",
|
||||
"dateWeekday": "qualsiasi giorno della settimana, userà la data più vicina",
|
||||
"dateCurrentYear": "userà l’anno corrente",
|
||||
"dateNth": "userà il {day} del mese corrente",
|
||||
"dateTime": "Combina uno qualsiasi dei formati di data con \"{time}\" (o {timePM}) per impostare un orario.",
|
||||
"repeats": "Attività ricorrenti",
|
||||
"repeatsDescription": "Per impostare un'attività come ricorrente in un intervallo, basta aggiungere '{suffix}' al testo dell'attività. La quantità deve essere un numero e può essere omesso per usare solo il tipo (vedi esempi)."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"title": "Teams",
|
||||
"title": "Gruppi",
|
||||
"noTeams": "Non fai parte di nessun gruppo.",
|
||||
"create": {
|
||||
"title": "Crea un nuovo gruppo",
|
||||
|
@ -746,23 +746,23 @@
|
|||
"makeAdmin": "Rendi Amministratore",
|
||||
"success": "Gruppo aggiornato.",
|
||||
"userAddedSuccess": "Membro del gruppo aggiunto.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"madeMember": "Membro del gruppo reso membro.",
|
||||
"madeAdmin": "Membro del gruppo reso amministratore.",
|
||||
"delete": {
|
||||
"header": "Elimina il gruppo",
|
||||
"text1": "Sei sicuro di voler eliminare questo gruppo e tutti i suoi membri?",
|
||||
"text2": "All team members will lose access to lists and namespaces shared with this team. This CANNOT BE UNDONE!",
|
||||
"text2": "Tutti i membri del gruppo perderanno l'accesso alle liste e ai namespace condivisi con questo gruppo. NON PUÒ ESSERE RIPRISTINATO!",
|
||||
"success": "Gruppo eliminato."
|
||||
},
|
||||
"deleteUser": {
|
||||
"header": "Rimuovi un utente dal gruppo",
|
||||
"text1": "Confermi di voler rimuovere questo utente dal gruppo?",
|
||||
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
|
||||
"text2": "Perderanno l'accesso a tutte le liste e i namespace a cui questo gruppo ha accesso. NON PUÒ ESSERE RIPRISTINATO!",
|
||||
"success": "Utente rimosso dal gruppo."
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"name": "Team Name",
|
||||
"name": "Nome Gruppo",
|
||||
"namePlaceholder": "Il nome del gruppo va qui…",
|
||||
"nameRequired": "Specifica un nome.",
|
||||
"description": "Descrizione",
|
||||
|
@ -772,32 +772,32 @@
|
|||
}
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"general": "General",
|
||||
"title": "Tasti Rapidi",
|
||||
"general": "Generali",
|
||||
"allPages": "Queste scorciatoie funzionano in tutte le pagine.",
|
||||
"currentPageOnly": "Queste scorciatoie funzionano solo nella pagina attuale.",
|
||||
"toggleMenu": "Attiva/Disattiva Menu",
|
||||
"quickSearch": "Apri la barra di ricerca/azione rapida",
|
||||
"then": "then",
|
||||
"then": "e dopo",
|
||||
"task": {
|
||||
"title": "Task Page",
|
||||
"done": "Done",
|
||||
"assign": "Assign to a user",
|
||||
"labels": "Add labels to this task",
|
||||
"dueDate": "Change the due date of this task",
|
||||
"attachment": "Add an attachment to this task",
|
||||
"related": "Modify related tasks of this task"
|
||||
"title": "Pagina Attività",
|
||||
"done": "Fatto",
|
||||
"assign": "Assegna a un utente",
|
||||
"labels": "Aggiungi etichette a questa attività",
|
||||
"dueDate": "Modifica la data di scadenza di questa attività",
|
||||
"attachment": "Aggiungi un allegato a questa attività",
|
||||
"related": "Modifica le attività collegate a questa"
|
||||
},
|
||||
"list": {
|
||||
"title": "List Views",
|
||||
"switchToListView": "Switch to list view",
|
||||
"switchToGanttView": "Switch to gantt view",
|
||||
"switchToKanbanView": "Switch to kanban view",
|
||||
"switchToTableView": "Switch to table view"
|
||||
"title": "Viste Liste",
|
||||
"switchToListView": "Passa alla vista Lista",
|
||||
"switchToGanttView": "Passa alla vista Gantt",
|
||||
"switchToKanbanView": "Passa alla vista Kanban",
|
||||
"switchToTableView": "Passa alla vista Tabella"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "È disponibile un aggiornamento per Vikunja!",
|
||||
"do": "Aggiorna Adesso"
|
||||
},
|
||||
"menu": {
|
||||
|
@ -805,136 +805,136 @@
|
|||
"archive": "Archivia",
|
||||
"duplicate": "Duplica",
|
||||
"delete": "Elimina",
|
||||
"unarchive": "Un-Archive",
|
||||
"setBackground": "Set background",
|
||||
"unarchive": "Disarchivia",
|
||||
"setBackground": "Imposta sfondo",
|
||||
"share": "Condividi",
|
||||
"newList": "Nuova lista"
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "URL Vikunja",
|
||||
"urlPlaceholder": "es. http://localhost:8080",
|
||||
"change": "change",
|
||||
"use": "Using Vikunja installation at {0}",
|
||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
||||
"success": "Using Vikunja installation at \"{domain}\".",
|
||||
"urlRequired": "A url is required."
|
||||
"change": "modifica",
|
||||
"use": "Usa l'installazione di Vikunja a {0}",
|
||||
"error": "Impossibile trovare o usare l'installazione di Vikunja su \"{domain}\". Prova per favore con un altro Url.",
|
||||
"success": "Utilizzando l'installazione di Vikunja su \"{domain}\".",
|
||||
"urlRequired": "L'URL è obbligatorio."
|
||||
},
|
||||
"loadingError": {
|
||||
"failed": "Loading failed, please {0}. If the error persists, please {1}.",
|
||||
"tryAgain": "try again",
|
||||
"contact": "contact us"
|
||||
"failed": "Caricamento non riuscito, si prega di {0}. Se l'errore persiste, per favore {1}.",
|
||||
"tryAgain": "riprova",
|
||||
"contact": "Contattaci"
|
||||
},
|
||||
"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."
|
||||
"title": "Notifiche",
|
||||
"none": "Nessuna notifica. Buona giornata!",
|
||||
"explainer": "Le notifiche appariranno qui quando le azioni su Namespace, liste o attività a cui hai sottoscritto la sottoscrizione avvengono."
|
||||
},
|
||||
"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.",
|
||||
"tasks": "Tasks",
|
||||
"commands": "Comandi",
|
||||
"placeholder": "Digita un comando o cerca…",
|
||||
"hint": "Puoi usare {list} per limitare la ricerca a una lista. Unisci {list} o {label} (etichette) alla ricerca per trovare un'attività con quelle etichette o in quella lista. Usa {assignee} per cercare solo i gruppi.",
|
||||
"tasks": "Attivitá",
|
||||
"lists": "Liste",
|
||||
"teams": "Teams",
|
||||
"newList": "Enter the title of the new list…",
|
||||
"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})",
|
||||
"teams": "Gruppi",
|
||||
"newList": "Inserisci il titolo della nuova lista…",
|
||||
"newTask": "Inserisci il titolo della nuova attività…",
|
||||
"newNamespace": "Inserisci il titolo del nuovo namespace…",
|
||||
"newTeam": "Inserisci il nome del nuovo gruppo…",
|
||||
"createTask": "Crea un'attività nella lista attuale ({title})",
|
||||
"createList": "Crea una lista nel namespace attuale ({title})",
|
||||
"cmds": {
|
||||
"newTask": "New task",
|
||||
"newList": "New list",
|
||||
"newNamespace": "New namespace",
|
||||
"newTeam": "New team"
|
||||
"newTask": "Nuova attività",
|
||||
"newList": "Nuova lista",
|
||||
"newNamespace": "Nuovo Namespace",
|
||||
"newTeam": "Nuovo gruppo"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"locale": "en",
|
||||
"locale": "it",
|
||||
"altFormatLong": "j M Y H:i",
|
||||
"altFormatShort": "j M Y"
|
||||
},
|
||||
"error": {
|
||||
"error": "Errore",
|
||||
"success": "Success",
|
||||
"success": "Fatto",
|
||||
"0001": "Non ti è permesso farlo.",
|
||||
"1001": "A user with this username already exists.",
|
||||
"1001": "Esiste già un utente con questo nome utente.",
|
||||
"1002": "Un utente con questo indirizzo e-mail esiste già.",
|
||||
"1004": "No username and password specified.",
|
||||
"1004": "Nessun nome utente e password specificati.",
|
||||
"1005": "L'utente non esiste.",
|
||||
"1006": "Impossibile ottenere l'id utente.",
|
||||
"1008": "No password reset token provided.",
|
||||
"1009": "Invalid password reset token.",
|
||||
"1008": "Nessun codice di reimpostazione password fornito.",
|
||||
"1009": "Codice di reimpostazione password non valido.",
|
||||
"1010": "Token di conferma dell'e-mail non valido.",
|
||||
"1011": "Wrong username or password.",
|
||||
"1011": "Nome utente o password errati.",
|
||||
"1012": "Indirizzo e-mail dell'utente non confermato.",
|
||||
"1013": "La nuova password è vuota.",
|
||||
"1014": "La vecchia password è vuota.",
|
||||
"1015": "Autenticazione TOTP già abilitata per questo utente.",
|
||||
"1016": "Autenticazione TOTP non abilitata per questo utente.",
|
||||
"1017": "Codice TOTP non valido.",
|
||||
"1018": "The user avatar type setting is invalid.",
|
||||
"1018": "L'impostazione del tipo di avatar utente non è valida.",
|
||||
"2001": "L'ID non può essere vuoto o 0.",
|
||||
"2002": "Alcuni dati della richiesta non erano validi.",
|
||||
"3001": "La lista non esiste.",
|
||||
"3004": "You need to have read permissions on that list to perform that action.",
|
||||
"3004": "Devi avere i permessi di lettura su quella lista per eseguire quell'azione.",
|
||||
"3005": "Il titolo della lista non può essere vuoto.",
|
||||
"3006": "The list share does not exist.",
|
||||
"3006": "La condivisione della lista non esiste.",
|
||||
"3007": "Esiste già una lista con questo identificatore.",
|
||||
"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.",
|
||||
"3008": "La lista è archiviata e può quindi essere consultata solo in sola lettura. Questo vale anche per tutte le attività associate a questa lista.",
|
||||
"4001": "Il testo delle attività della lista non può essere vuoto.",
|
||||
"4002": "Lista di attività non esistente.",
|
||||
"4003": "Tutte le attività di modifica in blocco devono appartenere alla stessa lista.",
|
||||
"4004": "Hai bisogno di almeno un'attività quando si modificano in blocco le attività.",
|
||||
"4005": "Non hai il permesso di vedere l'attività.",
|
||||
"4006": "You can't set a parent task as the task itself.",
|
||||
"4007": "You can't create a task relation with an invalid kind of relation.",
|
||||
"4008": "You can't create a task relation which already exists.",
|
||||
"4009": "The task relation does not exist.",
|
||||
"4010": "Cannot relate a task with itself.",
|
||||
"4011": "The task attachment does not exist.",
|
||||
"4012": "The task attachment is too large.",
|
||||
"4013": "The task sort param is invalid.",
|
||||
"4014": "The task sort order is invalid.",
|
||||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
"5006": "The namespace name cannot be empty.",
|
||||
"5009": "You need to have namespace read access to perform that action.",
|
||||
"5010": "This team does not have access to that namespace.",
|
||||
"5011": "This user has already access to that namespace.",
|
||||
"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.",
|
||||
"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.",
|
||||
"4006": "Non è possibile impostare un'attività principale come l'attività stessa.",
|
||||
"4007": "Non è possibile creare una relazione di attività con un tipo di relazione non valido.",
|
||||
"4008": "Non è possibile creare una relazione di attività già esistente.",
|
||||
"4009": "La relazione di attività non esiste.",
|
||||
"4010": "Non è possibile relazionare un'attività con se stessa.",
|
||||
"4011": "L'allegato dell'attività non esiste.",
|
||||
"4012": "L'allegato dell'attività è troppo grande.",
|
||||
"4013": "Il parametro di ordinamento dei task non è valido.",
|
||||
"4014": "L' ordinamento dei task non è valido.",
|
||||
"4015": "Il commento all'attività non esiste.",
|
||||
"4016": "Campo attività non valido.",
|
||||
"4017": "Comparatore di filtri attività non valido.",
|
||||
"4018": "Concatenatore filtro attività non valido.",
|
||||
"4019": "Filtro attività non valido.",
|
||||
"5001": "Il namespace non esiste.",
|
||||
"5003": "Non hai accesso a questo namespace.",
|
||||
"5006": "Il nome del namespace non può essere vuoto.",
|
||||
"5009": "Devi avere accesso in lettura al namespace per effettuare questa operazione.",
|
||||
"5010": "Il tuo gruppo non ha accesso a questo namespace.",
|
||||
"5011": "Questo utente ha già accesso a quel namespace.",
|
||||
"5012": "Il namespace è archiviato e può quindi essere accessibile solo in sola lettura.",
|
||||
"6001": "Il nome del gruppo non può essere vuoto.",
|
||||
"6002": "Gruppo non esistente.",
|
||||
"6004": "Il team ha già accesso a questo namespace o lista.",
|
||||
"6005": "L'utente è già membro di quel gruppo.",
|
||||
"6006": "Non è possibile eliminare l'ultimo membro del gruppo.",
|
||||
"6007": "Il gruppo non ha accesso alla lista per eseguire quell'azione.",
|
||||
"7002": "L'utente ha già accesso a quella lista.",
|
||||
"7003": "Non hai accesso a quella lista.",
|
||||
"8001": "Questa etichetta esiste già in quell'attività.",
|
||||
"8002": "L'etichetta non esiste.",
|
||||
"8003": "Non hai accesso a questa etichetta.",
|
||||
"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.",
|
||||
"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.",
|
||||
"11001": "The saved filter does not exist.",
|
||||
"11002": "Saved filters are not available for link shares.",
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"9001": "Permesso non valido.",
|
||||
"10001": "Colonna non esistente.",
|
||||
"10002": "La colonna non appartiene a quella lista.",
|
||||
"10003": "Non puoi rimuovere l'ultima colonna di una lista.",
|
||||
"10004": "Non puoi aggiungere l'attività a questa colonna perché ha già superato il limite di attività che può contenere.",
|
||||
"10005": "Ci può essere solo una colonna completati per lista.",
|
||||
"11001": "Filtro salvato non esistente.",
|
||||
"11002": "I filtri salvati non sono disponibili per i link di condivisione.",
|
||||
"12001": "Il tipo di entità sottoscritto non è valido.",
|
||||
"12002": "Sei già iscritto all'entità stessa o a un'entità principale.",
|
||||
"13001": "Questa condivisione di link richiede una password per l'autenticazione, ma non è stato inserita.",
|
||||
"13002": "La password inserita per il link di condivisione è valida."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"title": "Informazioni",
|
||||
"frontendVersion": "Versione Frontend: {version}",
|
||||
"apiVersion": "Versione API: {version}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "Комментарий не существует.",
|
||||
"4016": "Неверное поле задачи.",
|
||||
"4017": "Неверный сравнитель фильтров задач.",
|
||||
"4018": "Неверный соединитель фильтров задач.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Неверное значение фильтра задач.",
|
||||
"5001": "Пространство имён не существует.",
|
||||
"5003": "Нет доступа к указанному пространству имён.",
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "The task comment does not exist.",
|
||||
"4016": "Invalid task field.",
|
||||
"4017": "Invalid task filter comparator.",
|
||||
"4018": "Invalid task filter concatinator.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Invalid task filter value.",
|
||||
"5001": "The namespace does not exist.",
|
||||
"5003": "You do not have access to the specified namespace.",
|
||||
|
|
|
@ -899,7 +899,7 @@
|
|||
"4015": "Bình luận không tồn tại.",
|
||||
"4016": "Trường công việc không hợp lệ.",
|
||||
"4017": "Bộ so sánh bộ lọc công việc không hợp lệ.",
|
||||
"4018": "Bộ lọc kết hợp không hợp lệ.",
|
||||
"4018": "Invalid task filter concatenator.",
|
||||
"4019": "Giá trị bộ lọc công việc không hợp lệ.",
|
||||
"5001": "Góc làm việc không có nữa.",
|
||||
"5003": "Bạn chưa được phép bước vào vào góc làm việc được chỉ định.",
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
faCocktail,
|
||||
faCoffee,
|
||||
faCog,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faEllipsisH,
|
||||
faEllipsisV,
|
||||
faExclamation,
|
||||
|
@ -87,6 +89,8 @@ library.add(faCocktail)
|
|||
library.add(faCoffee)
|
||||
library.add(faCog)
|
||||
library.add(faComments)
|
||||
library.add(faEye)
|
||||
library.add(faEyeSlash)
|
||||
library.add(faEllipsisH)
|
||||
library.add(faEllipsisV)
|
||||
library.add(faExclamation)
|
||||
|
|
|
@ -18,7 +18,7 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
import {formatDate, formatDateShort, formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
|
||||
import {formatDate, formatDateShort, formatDateLong, formatDateSince, formatISO} from '@/helpers/time/formatDate'
|
||||
// @ts-ignore
|
||||
import {VERSION} from './version.json'
|
||||
|
||||
|
@ -52,6 +52,7 @@ app.use(Notifications)
|
|||
|
||||
// directives
|
||||
import focus from '@/directives/focus'
|
||||
// @ts-ignore The export does exist, ts just doesn't find it.
|
||||
import { VTooltip } from 'v-tooltip'
|
||||
import 'v-tooltip/dist/v-tooltip.css'
|
||||
import shortcut from '@/directives/shortcut'
|
||||
|
@ -84,6 +85,7 @@ app.mixin({
|
|||
format: formatDate,
|
||||
formatDate: formatDateLong,
|
||||
formatDateShort: formatDateShort,
|
||||
formatISO,
|
||||
getNamespaceTitle,
|
||||
getListTitle,
|
||||
setTitle,
|
||||
|
|
|
@ -10,9 +10,6 @@ import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
|||
const SUPPORTS_TRIGGERED_NOTIFICATION = 'Notification' in window && 'showTrigger' in Notification.prototype
|
||||
|
||||
export default class TaskModel extends AbstractModel {
|
||||
|
||||
defaultColor = '198CFF'
|
||||
|
||||
constructor(data) {
|
||||
super(data)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
import {beforeEach, afterEach, describe, it, expect, vi} from 'vitest'
|
||||
|
||||
import {parseTaskText} from './parseTaskText'
|
||||
import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate'
|
||||
|
@ -6,6 +6,14 @@ import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
|
|||
import priorities from '../models/constants/priorities.json'
|
||||
|
||||
describe('Parse Task Text', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return text with no intents as is', () => {
|
||||
expect(parseTaskText('Lorem Ipsum').text).toBe('Lorem Ipsum')
|
||||
})
|
||||
|
@ -211,17 +219,36 @@ describe('Parse Task Text', () => {
|
|||
expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe('14:0')
|
||||
})
|
||||
it('should recognize dates of the month in the past but next month', () => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - 1)
|
||||
const result = parseTaskText(`Lorem Ipsum ${date.getDate()}nd`)
|
||||
const time = new Date(2022, 0, 15)
|
||||
vi.setSystemTime(time)
|
||||
|
||||
const result = parseTaskText(`Lorem Ipsum ${time.getDate() - 1}th`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getDate()).toBe(date.getDate())
|
||||
expect(result.date.getDate()).toBe(time.getDate() - 1)
|
||||
expect(result.date.getMonth()).toBe(time.getMonth() + 1)
|
||||
})
|
||||
it('should recognize dates of the month in the past but next month when february is the next month', () => {
|
||||
const jan = new Date(2022, 0, 30)
|
||||
vi.setSystemTime(jan)
|
||||
|
||||
const nextMonthWithDate = result.date.getDate() === 31
|
||||
? (date.getMonth() + 2) % 12
|
||||
: (date.getMonth() + 1) % 12
|
||||
expect(result.date.getMonth()).toBe(nextMonthWithDate)
|
||||
const result = parseTaskText(`Lorem Ipsum ${jan.getDate() - 1}th`)
|
||||
|
||||
const expectedDate = new Date(2022, 2, jan.getDate() - 1)
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getDate()).toBe(expectedDate.getDate())
|
||||
expect(result.date.getMonth()).toBe(expectedDate.getMonth())
|
||||
})
|
||||
it('should recognize dates of the month in the past but next month when the next month has less days than this one', () => {
|
||||
const mar = new Date(2022, 2, 32)
|
||||
vi.setSystemTime(mar)
|
||||
|
||||
const result = parseTaskText(`Lorem Ipsum 31st`)
|
||||
|
||||
const expectedDate = new Date(2022, 4, 31)
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result.date.getDate()).toBe(expectedDate.getDate())
|
||||
expect(result.date.getMonth()).toBe(expectedDate.getMonth())
|
||||
})
|
||||
it('should recognize dates of the month in the future', () => {
|
||||
const nextDay = new Date(+new Date() + 60 * 60 * 24 * 1000)
|
||||
|
@ -242,6 +269,12 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum github')
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it('should not recognize date number with no spacing around them', () => {
|
||||
const result = parseTaskText('Lorem Ispum v1.1.1')
|
||||
|
||||
expect(result.text).toBe('Lorem Ispum v1.1.1')
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
|
||||
describe('Parse weekdays', () => {
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import { createRouter, createWebHistory, RouteLocation } from 'vue-router'
|
|||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
import {store} from '@/store'
|
||||
|
||||
import {saveListView, getListView} from '@/helpers/saveListView'
|
||||
|
||||
import HomeComponent from '../views/Home.vue'
|
||||
import NotFoundComponent from '../views/404.vue'
|
||||
import About from '../views/About.vue'
|
||||
|
@ -13,9 +15,8 @@ import DataExportDownload from '../views/user/DataExportDownload.vue'
|
|||
// Tasks
|
||||
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange.vue'
|
||||
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth.vue'
|
||||
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal.vue'
|
||||
import TaskDetailView from '../views/tasks/TaskDetailView.vue'
|
||||
import ListNamespaces from '../views/namespaces/ListNamespaces.vue'
|
||||
import TaskDetailView from '../views/tasks/TaskDetailView.vue'
|
||||
// Team Handling
|
||||
import ListTeamsComponent from '../views/teams/ListTeams.vue'
|
||||
// Label Handling
|
||||
|
@ -25,11 +26,11 @@ import NewLabelComponent from '../views/labels/NewLabel.vue'
|
|||
import MigrationComponent from '../views/migrator/Migrate.vue'
|
||||
import MigrateServiceComponent from '../views/migrator/MigrateService.vue'
|
||||
// List Views
|
||||
import ShowListComponent from '../views/list/ShowList.vue'
|
||||
import Kanban from '../views/list/views/Kanban.vue'
|
||||
import List from '../views/list/views/List.vue'
|
||||
import Gantt from '../views/list/views/Gantt.vue'
|
||||
import Table from '../views/list/views/Table.vue'
|
||||
import ListList from '../views/list/ListList.vue'
|
||||
import ListGantt from '../views/list/ListGantt.vue'
|
||||
import ListTable from '../views/list/ListTable.vue'
|
||||
import ListKanban from '../views/list/ListKanban.vue'
|
||||
|
||||
// List Settings
|
||||
import ListSettingEdit from '../views/list/settings/edit.vue'
|
||||
import ListSettingBackground from '../views/list/settings/background.vue'
|
||||
|
@ -80,7 +81,7 @@ const router = createRouter({
|
|||
|
||||
// Scroll to anchor should still work
|
||||
if (to.hash) {
|
||||
return {el: document.getElementById(to.hash.slice(1))}
|
||||
return {el: to.hash}
|
||||
}
|
||||
|
||||
// Otherwise just scroll to the top
|
||||
|
@ -132,7 +133,7 @@ const router = createRouter({
|
|||
name: 'user.register',
|
||||
component: RegisterComponent,
|
||||
meta: {
|
||||
title: 'user.auth.register',
|
||||
title: 'user.auth.createAccount',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -201,320 +202,170 @@ const router = createRouter({
|
|||
{
|
||||
path: '/namespaces/new',
|
||||
name: 'namespace.create',
|
||||
components: {
|
||||
popup: NewNamespaceComponent,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/namespaces/:id/list',
|
||||
name: 'list.create',
|
||||
components: {
|
||||
popup: NewListComponent,
|
||||
component: NewNamespaceComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/namespaces/:id/settings/edit',
|
||||
name: 'namespace.settings.edit',
|
||||
components: {
|
||||
popup: NamespaceSettingEdit,
|
||||
component: NamespaceSettingEdit,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/namespaces/:id/settings/share',
|
||||
path: '/namespaces/:namespaceId/settings/share',
|
||||
name: 'namespace.settings.share',
|
||||
components: {
|
||||
popup: NamespaceSettingShare,
|
||||
component: NamespaceSettingShare,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/namespaces/:id/settings/archive',
|
||||
name: 'namespace.settings.archive',
|
||||
components: {
|
||||
popup: NamespaceSettingArchive,
|
||||
component: NamespaceSettingArchive,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/namespaces/:id/settings/delete',
|
||||
name: 'namespace.settings.delete',
|
||||
components: {
|
||||
popup: NamespaceSettingDelete,
|
||||
component: NamespaceSettingDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tasks/:id',
|
||||
name: 'task.detail',
|
||||
component: TaskDetailView,
|
||||
props: route => ({ taskId: parseInt(route.params.id as string) }),
|
||||
},
|
||||
{
|
||||
path: '/tasks/by/upcoming',
|
||||
name: 'tasks.range',
|
||||
component: ShowTasksInRangeComponent,
|
||||
},
|
||||
{
|
||||
path: '/lists/new/:namespaceId/',
|
||||
name: 'list.create',
|
||||
component: NewListComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'list.settings.edit',
|
||||
components: {
|
||||
popup: ListSettingEdit,
|
||||
component: ListSettingEdit,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/background',
|
||||
name: 'list.settings.background',
|
||||
components: {
|
||||
popup: ListSettingBackground,
|
||||
component: ListSettingBackground,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/duplicate',
|
||||
name: 'list.settings.duplicate',
|
||||
components: {
|
||||
popup: ListSettingDuplicate,
|
||||
component: ListSettingDuplicate,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/share',
|
||||
name: 'list.settings.share',
|
||||
components: {
|
||||
popup: ListSettingShare,
|
||||
component: ListSettingShare,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'list.settings.delete',
|
||||
components: {
|
||||
popup: ListSettingDelete,
|
||||
component: ListSettingDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/archive',
|
||||
name: 'list.settings.archive',
|
||||
components: {
|
||||
popup: ListSettingArchive,
|
||||
component: ListSettingArchive,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.settings.edit',
|
||||
components: {
|
||||
popup: FilterEdit,
|
||||
component: FilterEdit,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.settings.delete',
|
||||
components: {
|
||||
popup: FilterDelete,
|
||||
component: FilterDelete,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId',
|
||||
name: 'list.index',
|
||||
component: ShowListComponent,
|
||||
children: [
|
||||
redirect(to) {
|
||||
// Redirect the user to list view by default
|
||||
|
||||
const savedListView = getListView(to.params.listId)
|
||||
console.debug('Replaced list view with', savedListView)
|
||||
|
||||
return {
|
||||
name: router.hasRoute(savedListView)
|
||||
? savedListView
|
||||
: 'list.list',
|
||||
params: {listId: to.params.listId},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/list',
|
||||
name: 'list.list',
|
||||
component: List,
|
||||
children: [
|
||||
{
|
||||
path: '/tasks/:id',
|
||||
name: 'task.list.detail',
|
||||
component: TaskDetailViewModal,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'list.list.settings.edit',
|
||||
component: ListSettingEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/background',
|
||||
name: 'list.list.settings.background',
|
||||
component: ListSettingBackground,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/duplicate',
|
||||
name: 'list.list.settings.duplicate',
|
||||
component: ListSettingDuplicate,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/share',
|
||||
name: 'list.list.settings.share',
|
||||
component: ListSettingShare,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'list.list.settings.delete',
|
||||
component: ListSettingDelete,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/archive',
|
||||
name: 'list.list.settings.archive',
|
||||
component: ListSettingArchive,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.list.settings.edit',
|
||||
component: FilterEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.list.settings.delete',
|
||||
component: FilterDelete,
|
||||
},
|
||||
],
|
||||
component: ListList,
|
||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
||||
props: route => ({ listId: parseInt(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/gantt',
|
||||
name: 'list.gantt',
|
||||
component: Gantt,
|
||||
children: [
|
||||
{
|
||||
path: '/tasks/:id',
|
||||
name: 'task.gantt.detail',
|
||||
component: TaskDetailViewModal,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'list.gantt.settings.edit',
|
||||
component: ListSettingEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/background',
|
||||
name: 'list.gantt.settings.background',
|
||||
component: ListSettingBackground,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/duplicate',
|
||||
name: 'list.gantt.settings.duplicate',
|
||||
component: ListSettingDuplicate,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/share',
|
||||
name: 'list.gantt.settings.share',
|
||||
component: ListSettingShare,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'list.gantt.settings.delete',
|
||||
component: ListSettingDelete,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/archive',
|
||||
name: 'list.gantt.settings.archive',
|
||||
component: ListSettingArchive,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.gantt.settings.edit',
|
||||
component: FilterEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.gantt.settings.delete',
|
||||
component: FilterDelete,
|
||||
},
|
||||
],
|
||||
component: ListGantt,
|
||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
||||
props: route => ({ listId: parseInt(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/table',
|
||||
name: 'list.table',
|
||||
component: Table,
|
||||
children: [
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'list.table.settings.edit',
|
||||
component: ListSettingEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/background',
|
||||
name: 'list.table.settings.background',
|
||||
component: ListSettingBackground,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/duplicate',
|
||||
name: 'list.table.settings.duplicate',
|
||||
component: ListSettingDuplicate,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/share',
|
||||
name: 'list.table.settings.share',
|
||||
component: ListSettingShare,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'list.table.settings.delete',
|
||||
component: ListSettingDelete,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/archive',
|
||||
name: 'list.table.settings.archive',
|
||||
component: ListSettingArchive,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.table.settings.edit',
|
||||
component: FilterEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.table.settings.delete',
|
||||
component: FilterDelete,
|
||||
},
|
||||
],
|
||||
component: ListTable,
|
||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
||||
props: route => ({ listId: parseInt(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/kanban',
|
||||
name: 'list.kanban',
|
||||
component: Kanban,
|
||||
children: [
|
||||
{
|
||||
path: '/tasks/:id',
|
||||
name: 'task.kanban.detail',
|
||||
component: TaskDetailViewModal,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'list.kanban.settings.edit',
|
||||
component: ListSettingEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/background',
|
||||
name: 'list.kanban.settings.background',
|
||||
component: ListSettingBackground,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/duplicate',
|
||||
name: 'list.kanban.settings.duplicate',
|
||||
component: ListSettingDuplicate,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/share',
|
||||
name: 'list.kanban.settings.share',
|
||||
component: ListSettingShare,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'list.kanban.settings.delete',
|
||||
component: ListSettingDelete,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/archive',
|
||||
name: 'list.kanban.settings.archive',
|
||||
component: ListSettingArchive,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
name: 'filter.kanban.settings.edit',
|
||||
component: FilterEdit,
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
name: 'filter.kanban.settings.delete',
|
||||
component: FilterDelete,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
component: ListKanban,
|
||||
beforeEnter: (to) => saveListView(to.params.listId, to.name),
|
||||
props: route => ({ listId: parseInt(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/teams',
|
||||
|
@ -524,8 +375,9 @@ const router = createRouter({
|
|||
{
|
||||
path: '/teams/new',
|
||||
name: 'teams.create',
|
||||
components: {
|
||||
popup: NewTeamComponent,
|
||||
component: NewTeamComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -541,8 +393,9 @@ const router = createRouter({
|
|||
{
|
||||
path: '/labels/new',
|
||||
name: 'labels.create',
|
||||
components: {
|
||||
popup: NewLabelComponent,
|
||||
component: NewLabelComponent,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -558,8 +411,9 @@ const router = createRouter({
|
|||
{
|
||||
path: '/filters/new',
|
||||
name: 'filters.create',
|
||||
components: {
|
||||
popup: FilterNew,
|
||||
component: FilterNew,
|
||||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -575,11 +429,7 @@ const router = createRouter({
|
|||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
return checkAuth(to)
|
||||
})
|
||||
|
||||
function checkAuth(route: RouteLocation) {
|
||||
export function getAuthForRoute(route: RouteLocation) {
|
||||
const authUser = store.getters['auth/authUser']
|
||||
const authLinkShare = store.getters['auth/authLinkShare']
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ import lists from './modules/lists'
|
|||
import attachments from './modules/attachments'
|
||||
import labels from './modules/labels'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
|
||||
import ListService from '../services/list'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
|
@ -37,13 +39,15 @@ export const store = createStore({
|
|||
loading: false,
|
||||
loadingModule: null,
|
||||
// This is used to highlight the current list in menu for all list related views
|
||||
currentList: {id: 0},
|
||||
currentList: new ListModel({
|
||||
id: 0,
|
||||
isArchived: false,
|
||||
}),
|
||||
background: '',
|
||||
hasTasks: false,
|
||||
menuActive: true,
|
||||
keyboardShortcutsActive: false,
|
||||
quickActionsActive: false,
|
||||
vikunjaReady: false,
|
||||
},
|
||||
mutations: {
|
||||
[LOADING](state, loading) {
|
||||
|
@ -79,9 +83,6 @@ export const store = createStore({
|
|||
[BACKGROUND](state, background) {
|
||||
state.background = background
|
||||
},
|
||||
vikunjaReady(state, ready) {
|
||||
state.vikunjaReady = ready
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async [CURRENT_LIST]({state, commit}, currentList) {
|
||||
|
@ -136,10 +137,9 @@ export const store = createStore({
|
|||
|
||||
commit(CURRENT_LIST, currentList)
|
||||
},
|
||||
async loadApp({commit, dispatch}) {
|
||||
async loadApp({dispatch}) {
|
||||
await checkAndSetApiUrl(window.API_URL)
|
||||
await dispatch('auth/checkAuth')
|
||||
commit('vikunjaReady', true)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import {HTTPFactory} from '@/http-common'
|
||||
import {getCurrentLanguage, saveLanguage} from '@/i18n'
|
||||
import {i18n, getCurrentLanguage, saveLanguage} from '@/i18n'
|
||||
import {LOADING} from '../mutation-types'
|
||||
import UserModel from '@/models/user'
|
||||
import UserSettingsService from '@/services/userSettings'
|
||||
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
|
||||
import {setLoading} from '@/store/helper'
|
||||
import {i18n} from '@/i18n'
|
||||
import {success} from '@/message'
|
||||
import {redirectToProvider} from '@/helpers/redirectToProvider'
|
||||
|
||||
const AUTH_TYPES = {
|
||||
'UNKNOWN': 0,
|
||||
|
@ -201,7 +201,19 @@ export default {
|
|||
ctx.commit('authenticated', authenticated)
|
||||
if (!authenticated) {
|
||||
ctx.commit('info', null)
|
||||
ctx.dispatch('config/redirectToProviderIfNothingElseIsEnabled', null, {root: true})
|
||||
ctx.dispatch('redirectToProviderIfNothingElseIsEnabled')
|
||||
}
|
||||
},
|
||||
|
||||
redirectToProviderIfNothingElseIsEnabled({rootState}) {
|
||||
const {auth} = rootState.config
|
||||
if (
|
||||
auth.local.enabled === false &&
|
||||
auth.openidConnect.enabled &&
|
||||
auth.openidConnect.providers?.length === 1 &&
|
||||
window.location.pathname.startsWith('/login') // Kinda hacky, but prevents an endless loop.
|
||||
) {
|
||||
redirectToProvider(auth.openidConnect.providers[0], auth.openidConnect.redirectUrl)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -226,7 +238,7 @@ export default {
|
|||
commit('info', info)
|
||||
commit('lastUserRefresh')
|
||||
|
||||
if (typeof info.settings.language !== 'undefined') {
|
||||
if (typeof info.settings.language === 'undefined' || info.settings.language === '') {
|
||||
// save current language
|
||||
await dispatch('saveUserSettings', {
|
||||
settings: {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {CONFIG} from '../mutation-types'
|
||||
import {HTTPFactory} from '@/http-common'
|
||||
import {objectToCamelCase} from '@/helpers/case'
|
||||
import {redirectToProvider} from '../../helpers/redirectToProvider'
|
||||
import {parseURL} from 'ufo'
|
||||
|
||||
export default {
|
||||
|
@ -75,16 +74,5 @@ export default {
|
|||
ctx.commit(CONFIG, info)
|
||||
return info
|
||||
},
|
||||
|
||||
redirectToProviderIfNothingElseIsEnabled(ctx) {
|
||||
if (ctx.state.auth.local.enabled === false &&
|
||||
ctx.state.auth.openidConnect.enabled &&
|
||||
ctx.state.auth.openidConnect.providers &&
|
||||
ctx.state.auth.openidConnect.providers.length === 1 &&
|
||||
window.location.pathname.startsWith('/login') // Kinda hacky, but prevents an endless loop.
|
||||
) {
|
||||
redirectToProvider(ctx.state.auth.openidConnect.providers[0])
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
|
@ -23,8 +23,6 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
// FIXME: direct manipulation of the prop
|
||||
// might not be a problem since this is happening in the mutation
|
||||
if (!namespace.lists || namespace.lists.length === 0) {
|
||||
namespace.lists = state.namespaces[namespaceIndex].lists
|
||||
}
|
||||
|
@ -136,8 +134,8 @@ export default {
|
|||
},
|
||||
|
||||
loadNamespacesIfFavoritesDontExist(ctx) {
|
||||
// The first namespace should be the one holding all favorites
|
||||
if (ctx.state.namespaces[0].id !== -2) {
|
||||
// The first or second namespace should be the one holding all favorites
|
||||
if (ctx.state.namespaces[0].id !== -2 && ctx.state.namespaces[1]?.id !== -2) {
|
||||
return ctx.dispatch('loadNamespaces')
|
||||
}
|
||||
},
|
||||
|
|
|
@ -179,9 +179,18 @@ export default {
|
|||
console.debug('Could not add label to task in kanban, task not found', t)
|
||||
return r
|
||||
}
|
||||
// FIXME: direct store manipulation (task)
|
||||
t.task.labels.push(label)
|
||||
ctx.commit('kanban/setTaskInBucketByIndex', t, { root: true })
|
||||
|
||||
const labels = [...t.task.labels]
|
||||
labels.push(label)
|
||||
|
||||
ctx.commit('kanban/setTaskInBucketByIndex', {
|
||||
task: {
|
||||
labels,
|
||||
...t.task,
|
||||
},
|
||||
...t,
|
||||
}, { root: true })
|
||||
|
||||
return r
|
||||
},
|
||||
|
||||
|
@ -200,15 +209,21 @@ export default {
|
|||
}
|
||||
|
||||
// Remove the label from the list
|
||||
for (const l in t.task.labels) {
|
||||
if (t.task.labels[l].id === label.id) {
|
||||
// FIXME: direct store manipulation (task)
|
||||
t.task.labels.splice(l, 1)
|
||||
const labels = [...t.task.labels]
|
||||
for (const l in labels) {
|
||||
if (labels[l].id === label.id) {
|
||||
labels.splice(l, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx.commit('kanban/setTaskInBucketByIndex', t, {root: true})
|
||||
ctx.commit('kanban/setTaskInBucketByIndex', {
|
||||
task: {
|
||||
labels,
|
||||
...t.task,
|
||||
},
|
||||
...t,
|
||||
}, {root: true})
|
||||
|
||||
return response
|
||||
},
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
// since $tablet is defined by bulma we can just define it after importing the utilities
|
||||
$mobile: math.div($tablet, 2);
|
||||
|
||||
@import "mixins";
|
||||
|
||||
$family-sans-serif: 'Open Sans', Helvetica, Arial, sans-serif;
|
||||
$vikunja-font: 'Quicksand', sans-serif;
|
||||
|
||||
|
|
|
@ -60,9 +60,9 @@
|
|||
--danger: hsla(var(--danger-h), var(--danger-s), var(--danger-l), var(--danger-a));
|
||||
|
||||
// var(--primary) / $blue is #1973ff
|
||||
--primary-h: 216.5deg;
|
||||
--primary-s: 100%;
|
||||
--primary-l: 54.9%;
|
||||
--primary-h: 217deg;
|
||||
--primary-s: 98%;
|
||||
--primary-l: 53%;
|
||||
--primary-a: 1;
|
||||
--primary-hsl: var(--primary-h), var(--primary-s), var(--primary-l);
|
||||
--primary: hsla(var(--primary-h), var(--primary-s), var(--primary-l), var(--primary-a));
|
||||
|
@ -122,5 +122,10 @@
|
|||
// Custom color variables we need to override
|
||||
--card-border-color: hsla(var(--grey-100-hsl), 0.3);
|
||||
--logo-text-color: var(--grey-700);
|
||||
|
||||
// Slightly different primary color to make sure it has a sufficent contrast ratio
|
||||
--primary-h: 217deg;
|
||||
--primary-s: 98%;
|
||||
--primary-l: 58%;
|
||||
}
|
||||
}
|
12
src/styles/mixins.scss
Normal file
12
src/styles/mixins.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
/* Transitions */
|
||||
@mixin modal-transition() {
|
||||
.modal-enter,
|
||||
.modal-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave-active .modal-container {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
.box,
|
||||
.card,
|
||||
.switch-view,
|
||||
.table-view .button,
|
||||
.list-table .button,
|
||||
.filter-container .button,
|
||||
.search .button {
|
||||
box-shadow: none;
|
||||
|
|
4
src/types/faker.d.ts
vendored
Normal file
4
src/types/faker.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module '@faker-js/faker' {
|
||||
import faker from 'faker'
|
||||
export default faker
|
||||
}
|
3
src/types/shims-vue.d.ts
vendored
3
src/types/shims-vue.d.ts
vendored
|
@ -3,6 +3,9 @@ declare module 'vue' {
|
|||
const Vue: CompatVue
|
||||
export default Vue
|
||||
export * from '@vue/runtime-dom'
|
||||
|
||||
const { configureCompat } = Vue
|
||||
export { configureCompat }
|
||||
}
|
||||
|
||||
// https://next.vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html#typescript-support
|
||||
|
|
1
src/types/vue-flatpickr-component.d.ts
vendored
Normal file
1
src/types/vue-flatpickr-component.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module 'vue-flatpickr-component';
|
|
@ -23,7 +23,7 @@
|
|||
<template v-if="defaultNamespaceId > 0">
|
||||
<p class="mt-4">{{ $t('home.list.newText') }}</p>
|
||||
<x-button
|
||||
:to="{ name: 'list.create', params: { id: defaultNamespaceId } }"
|
||||
:to="{ name: 'list.create', params: { namespaceId: defaultNamespaceId } }"
|
||||
:shadow="false"
|
||||
class="ml-2"
|
||||
>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue