diff --git a/.drone.yml b/.drone.yml
index 4090e6a8..a7522020 100644
--- a/.drone.yml
+++ b/.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
...
diff --git a/cypress.json b/cypress.json
index 27f12495..28fd022c 100644
--- a/cypress.json
+++ b/cypress.json
@@ -7,5 +7,6 @@
"video": false,
"retries": {
"runMode": 2
- }
+ },
+ "projectId": "181c7x"
}
diff --git a/cypress/factories/bucket.js b/cypress/factories/bucket.js
index be90cca9..8001899b 100644
--- a/cypress/factories/bucket.js
+++ b/cypress/factories/bucket.js
@@ -1,4 +1,4 @@
-import faker from 'faker'
+import faker from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
diff --git a/cypress/factories/labels.js b/cypress/factories/labels.js
index b3f9ab30..7aac5eb0 100644
--- a/cypress/factories/labels.js
+++ b/cypress/factories/labels.js
@@ -1,4 +1,4 @@
-import faker from 'faker'
+import faker from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
diff --git a/cypress/factories/link_sharing.js b/cypress/factories/link_sharing.js
index e2c01dd0..3a406ea2 100644
--- a/cypress/factories/link_sharing.js
+++ b/cypress/factories/link_sharing.js
@@ -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'
diff --git a/cypress/factories/list.js b/cypress/factories/list.js
index f93cdba4..2ffc3125 100644
--- a/cypress/factories/list.js
+++ b/cypress/factories/list.js
@@ -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'
diff --git a/cypress/factories/namespace.js b/cypress/factories/namespace.js
index 89096d2d..203f7159 100644
--- a/cypress/factories/namespace.js
+++ b/cypress/factories/namespace.js
@@ -1,4 +1,4 @@
-import faker from 'faker'
+import faker from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
diff --git a/cypress/factories/task.js b/cypress/factories/task.js
index 6fa8d5b6..5410a25e 100644
--- a/cypress/factories/task.js
+++ b/cypress/factories/task.js
@@ -1,4 +1,4 @@
-import faker from 'faker'
+import faker from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
diff --git a/cypress/factories/task_comment.js b/cypress/factories/task_comment.js
index 74e043f9..7800c009 100644
--- a/cypress/factories/task_comment.js
+++ b/cypress/factories/task_comment.js
@@ -1,4 +1,4 @@
-import faker from 'faker'
+import faker from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
diff --git a/cypress/factories/team.js b/cypress/factories/team.js
index 928b8ce4..33cc3794 100644
--- a/cypress/factories/team.js
+++ b/cypress/factories/team.js
@@ -1,4 +1,4 @@
-import faker from 'faker'
+import faker from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
diff --git a/cypress/factories/user.js b/cypress/factories/user.js
index 9e133b55..93971efe 100644
--- a/cypress/factories/user.js
+++ b/cypress/factories/user.js
@@ -1,4 +1,4 @@
-import faker from 'faker'
+import faker from '@faker-js/faker'
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
diff --git a/cypress/integration/list/list-history.spec.js b/cypress/integration/list/list-history.spec.js
new file mode 100644
index 00000000..b7633cbd
--- /dev/null
+++ b/cypress/integration/list/list-history.spec.js
@@ -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)
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/list/list-view-gantt.spec.js b/cypress/integration/list/list-view-gantt.spec.js
new file mode 100644
index 00000000..69805a30
--- /dev/null
+++ b/cypress/integration/list/list-view-gantt.spec.js
@@ -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})
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/list/list-view-kanban.spec.js b/cypress/integration/list/list-view-kanban.spec.js
new file mode 100644
index 00000000..52d67282
--- /dev/null
+++ b/cypress/integration/list/list-view-kanban.spec.js
@@ -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)
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/list/list-view-list.spec.js b/cypress/integration/list/list-view-list.spec.js
new file mode 100644
index 00000000..e1a4a0f6
--- /dev/null
+++ b/cypress/integration/list/list-view-list.spec.js
@@ -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)
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/list/list-view-table.spec.js b/cypress/integration/list/list-view-table.spec.js
new file mode 100644
index 00000000..e0336efc
--- /dev/null
+++ b/cypress/integration/list/list-view-table.spec.js
@@ -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}`)
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/list/list.spec.js b/cypress/integration/list/list.spec.js
index 55afcc7b..00f5b4f5 100644
--- a/cypress/integration/list/list.spec.js
+++ b/cypress/integration/list/list.spec.js
@@ -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)
- })
- })
})
diff --git a/cypress/integration/list/prepareLists.js b/cypress/integration/list/prepareLists.js
new file mode 100644
index 00000000..afef6ba4
--- /dev/null
+++ b/cypress/integration/list/prepareLists.js
@@ -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()
+ })
+}
\ No newline at end of file
diff --git a/cypress/integration/task/task.spec.js b/cypress/integration/task/task.spec.js
index 1b85e992..62343c91 100644
--- a/cypress/integration/task/task.spec.js
+++ b/cypress/integration/task/task.spec.js
@@ -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')
diff --git a/cypress/integration/user/registration.spec.js b/cypress/integration/user/registration.spec.js
index fd940aa7..16e959d7 100644
--- a/cypress/integration/user/registration.spec.js
+++ b/cypress/integration/user/registration.spec.js
@@ -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.')
})
diff --git a/cypress/integration/user/settings.spec.js b/cypress/integration/user/settings.spec.js
index c6a645d5..21bd9c1d 100644
--- a/cypress/integration/user/settings.spec.js
+++ b/cypress/integration/user/settings.spec.js
@@ -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')
+ cy.get('input[type=file]', {timeout: 1000})
+ .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')
})
diff --git a/cypress/support/index.js b/cypress/support/index.js
index 0c885c65..7b0c56d1 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -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
diff --git a/package.json b/package.json
index 9ab35adb..9adfbbbe 100644
--- a/package.json
+++ b/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.*",
diff --git a/renovate.json b/renovate.json
index 2019a5e4..440fcf3f 100644
--- a/renovate.json
+++ b/renovate.json
@@ -3,5 +3,11 @@
"labels": ["dependencies"],
"extends": [
"config:base"
+ ],
+ "packageRules": [
+ {
+ "matchPackageNames": ["netlify-cli"],
+ "extends": ["schedule:weekly"]
+ }
]
}
diff --git a/src/App.vue b/src/App.vue
index 06a81850..5ab3635f 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -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'])
diff --git a/src/assets/logo-full-pride.svg b/src/assets/logo-full-pride.svg
index 1ecacb3e..bff6f8bb 100644
--- a/src/assets/logo-full-pride.svg
+++ b/src/assets/logo-full-pride.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/assets/logo-full.svg b/src/assets/logo-full.svg
index 20b6ae13..db656b85 100644
--- a/src/assets/logo-full.svg
+++ b/src/assets/logo-full.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/components/home/contentAuth.vue b/src/components/home/contentAuth.vue
index d3f0dc8a..25fd01e7 100644
--- a/src/components/home/contentAuth.vue
+++ b/src/components/home/contentAuth.vue
@@ -1,33 +1,51 @@
-
+
-
-
+
-
-
-
+
+
-
+
+
+
+
+
+
+
-
+
+
+
\ No newline at end of file
diff --git a/src/components/list/list-settings-dropdown.vue b/src/components/list/list-settings-dropdown.vue
index 35fc6356..b8a9082b 100644
--- a/src/components/list/list-settings-dropdown.vue
+++ b/src/components/list/list-settings-dropdown.vue
@@ -2,21 +2,22 @@
{{ $t('menu.edit') }}
{{ $t('misc.delete') }}
+
{{ $t('menu.unarchive') }}
@@ -24,37 +25,38 @@
{{ $t('menu.edit') }}
{{ $t('menu.setBackground') }}
{{ $t('menu.share') }}
{{ $t('menu.duplicate') }}
{{ $t('menu.archive') }}
subscription = sub"
/>
@@ -73,56 +75,32 @@
-
diff --git a/src/components/list/partials/filter-popup.vue b/src/components/list/partials/filter-popup.vue
index efbfcbe3..d4caf9b4 100644
--- a/src/components/list/partials/filter-popup.vue
+++ b/src/components/list/partials/filter-popup.vue
@@ -29,9 +29,10 @@
diff --git a/src/components/misc/keyboard-shortcuts/shortcuts.js b/src/components/misc/keyboard-shortcuts/shortcuts.ts
similarity index 78%
rename from src/components/misc/keyboard-shortcuts/shortcuts.js
rename to src/components/misc/keyboard-shortcuts/shortcuts.ts
index bcc5014b..f68b7c35 100644
--- a/src/components/misc/keyboard-shortcuts/shortcuts.js
+++ b/src/components/misc/keyboard-shortcuts/shortcuts.ts
@@ -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',
diff --git a/src/components/misc/message.vue b/src/components/misc/message.vue
index df60cc38..7ff84f9f 100644
--- a/src/components/misc/message.vue
+++ b/src/components/misc/message.vue
@@ -1,18 +1,35 @@
-
-.modal-enter .modal-container,
-.modal-leave-active .modal-container {
- transform: scale(0.9);
+
\ No newline at end of file
diff --git a/src/components/namespace/namespace-search.vue b/src/components/namespace/namespace-search.vue
index 19a5c3b2..1dd1354e 100644
--- a/src/components/namespace/namespace-search.vue
+++ b/src/components/namespace/namespace-search.vue
@@ -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)
}
diff --git a/src/components/namespace/namespace-settings-dropdown.vue b/src/components/namespace/namespace-settings-dropdown.vue
index c2752578..3359fed9 100644
--- a/src/components/namespace/namespace-settings-dropdown.vue
+++ b/src/components/namespace/namespace-settings-dropdown.vue
@@ -16,13 +16,13 @@
{{ $t('menu.edit') }}
{{ $t('menu.share') }}
{{ $t('menu.newList') }}
@@ -34,6 +34,7 @@
{{ $t('menu.archive') }}
\ No newline at end of file
diff --git a/src/components/sharing/userTeam.vue b/src/components/sharing/userTeam.vue
index 007cfaa6..7b7debdd 100644
--- a/src/components/sharing/userTeam.vue
+++ b/src/components/sharing/userTeam.vue
@@ -365,3 +365,7 @@ export default {
},
}
+
+
\ No newline at end of file
diff --git a/src/components/tasks/edit-task.vue b/src/components/tasks/edit-task.vue
index e4ada125..718304c6 100644
--- a/src/components/tasks/edit-task.vue
+++ b/src/components/tasks/edit-task.vue
@@ -67,7 +67,7 @@
{{ $t('task.openDetail') }}
@@ -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,
diff --git a/src/components/tasks/mixins/taskList.js b/src/components/tasks/mixins/taskList.js
deleted file mode 100644
index a9b2e587..00000000
--- a/src/components/tasks/mixins/taskList.js
+++ /dev/null
@@ -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)
- }
- },
- },
-}
\ No newline at end of file
diff --git a/src/components/tasks/partials/attachments.vue b/src/components/tasks/partials/attachments.vue
index a01a6201..7d507e5e 100644
--- a/src/components/tasks/partials/attachments.vue
+++ b/src/components/tasks/partials/attachments.vue
@@ -34,7 +34,7 @@
>
{{ a.file.name }}
-
+
{{ 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();
\ No newline at end of file
diff --git a/src/components/tasks/partials/comments.vue b/src/components/tasks/partials/comments.vue
index d5db45c3..23105771 100644
--- a/src/components/tasks/partials/comments.vue
+++ b/src/components/tasks/partials/comments.vue
@@ -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();
\ No newline at end of file
diff --git a/src/components/tasks/partials/date-table-cell.vue b/src/components/tasks/partials/date-table-cell.vue
index e405c0b3..8df948c7 100644
--- a/src/components/tasks/partials/date-table-cell.vue
+++ b/src/components/tasks/partials/date-table-cell.vue
@@ -1,6 +1,8 @@
- {{ +date === 0 ? '-' : formatDateSince(date) }}
+
+ {{ +date === 0 ? '-' : formatDateSince(date) }}
+
diff --git a/src/components/tasks/partials/description.vue b/src/components/tasks/partials/description.vue
index 28fd5a88..07d136fd 100644
--- a/src/components/tasks/partials/description.vue
+++ b/src/components/tasks/partials/description.vue
@@ -38,7 +38,7 @@ import {mapState} from 'vuex'
export default {
name: 'description',
components: {
- editor: AsyncEditor,
+ Editor: AsyncEditor,
},
data() {
return {
diff --git a/src/components/tasks/partials/editLabels.vue b/src/components/tasks/partials/editLabels.vue
index ddc10dc3..f2d01934 100644
--- a/src/components/tasks/partials/editLabels.vue
+++ b/src/components/tasks/partials/editLabels.vue
@@ -19,19 +19,19 @@
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
class="tag">
{{ props.item.title }}
-
+
+ class="tag search-result">
{{ props.option }}
+ class="tag search-result">
{{ props.option.title }}
@@ -114,23 +114,17 @@ export default {
},
async removeLabel(label) {
- const removeFromState = () => {
- for (const l in this.labels) {
- if (this.labels[l].id === label.id) {
- this.labels.splice(l, 1)
- }
+ 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)
}
- 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.$emit('update:modelValue', this.labels)
+ this.$emit('change', this.labels)
this.$message.success({message: this.$t('task.label.removeSuccess')})
},
@@ -152,6 +146,18 @@ export default {
diff --git a/src/components/tasks/partials/kanban-card.vue b/src/components/tasks/partials/kanban-card.vue
index e1910643..d0d4501d 100644
--- a/src/components/tasks/partials/kanban-card.vue
+++ b/src/components/tasks/partials/kanban-card.vue
@@ -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)"
>
@@ -28,9 +28,9 @@
-
+
{{ formatDateSince(task.dueDate) }}
-
+
{{ task.title }}
diff --git a/src/components/tasks/partials/relatedTasks.vue b/src/components/tasks/partials/relatedTasks.vue
index 0b2be060..80c04e04 100644
--- a/src/components/tasks/partials/relatedTasks.vue
+++ b/src/components/tasks/partials/relatedTasks.vue
@@ -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();
\ No newline at end of file
diff --git a/src/components/tasks/partials/singleTaskInList.vue b/src/components/tasks/partials/singleTaskInList.vue
index 9e955d5c..85293dec 100644
--- a/src/components/tasks/partials/singleTaskInList.vue
+++ b/src/components/tasks/partials/singleTaskInList.vue
@@ -8,7 +8,7 @@
>
@@ -39,14 +39,17 @@
:user="a"
v-for="(a, i) in task.assignees"
/>
-
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
-
+
@@ -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) {
diff --git a/src/components/tasks/partials/sort.vue b/src/components/tasks/partials/sort.vue
index c3581ba5..4eeb5e9f 100644
--- a/src/components/tasks/partials/sort.vue
+++ b/src/components/tasks/partials/sort.vue
@@ -1,20 +1,21 @@
-
+
-
+
-
+
-
diff --git a/src/composables/taskList.js b/src/composables/taskList.js
new file mode 100644
index 00000000..e80c5cbe
--- /dev/null
+++ b/src/composables/taskList.js
@@ -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,
+ }
+}
\ No newline at end of file
diff --git a/src/composables/useColorScheme.ts b/src/composables/useColorScheme.ts
index c0fed121..f9b1cb94 100644
--- a/src/composables/useColorScheme.ts
+++ b/src/composables/useColorScheme.ts
@@ -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(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
+ const store = useStorage(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
const preferredColorScheme = usePreferredColorScheme()
diff --git a/src/composables/useTitle.ts b/src/composables/useTitle.ts
index e3544829..84aaa177 100644
--- a/src/composables/useTitle.ts
+++ b/src/composables/useTitle.ts
@@ -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(titleGetter: ComputedGetter) : ComputedRef {
+export function useTitle(titleGetter: ComputedGetter) {
const titleRef = computed(titleGetter)
watchEffect(() => setTitle(titleRef.value))
diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts
index b073a8ca..81cd5580 100644
--- a/src/helpers/auth.ts
+++ b/src/helpers/auth.ts
@@ -53,6 +53,7 @@ export async function refreshToken(persist: boolean): Promise {
return response
} catch(e) {
+ // @ts-ignore
throw new Error('Error renewing token: ', { cause: e })
}
}
diff --git a/src/helpers/isEmail.ts b/src/helpers/isEmail.ts
new file mode 100644
index 00000000..08957d0f
--- /dev/null
+++ b/src/helpers/isEmail.ts
@@ -0,0 +1,6 @@
+export function isEmail(email: string): Boolean {
+ const format = /^.+@.+$/
+ const match = email.match(format)
+
+ return match === null ? false : match.length > 0
+}
diff --git a/src/helpers/saveListView.js b/src/helpers/saveListView.js
index b7f735e1..96be6342 100644
--- a/src/helpers/saveListView.js
+++ b/src/helpers/saveListView.js
@@ -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
diff --git a/src/helpers/setTitle.js b/src/helpers/setTitle.ts
similarity index 65%
rename from src/helpers/setTitle.js
rename to src/helpers/setTitle.ts
index a2f31a45..d7e6b5dd 100644
--- a/src/helpers/setTitle.js
+++ b/src/helpers/setTitle.ts
@@ -1,4 +1,4 @@
-export function setTitle(title) {
+export function setTitle(title : undefined | string) {
document.title = (typeof title === 'undefined' || title === '')
? 'Vikunja'
: `${title} | Vikunja`
diff --git a/src/helpers/time/formatDate.js b/src/helpers/time/formatDate.js
index c3e8c1b7..afa3f70a 100644
--- a/src/helpers/time/formatDate.js
+++ b/src/helpers/time/formatDate.js
@@ -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) : ''
+}
diff --git a/src/helpers/time/parseDate.ts b/src/helpers/time/parseDate.ts
index 34e6ef0f..3b663469 100644
--- a/src/helpers/time/parseDate.ts
+++ b/src/helpers/time/parseDate.ts
@@ -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,17 +302,17 @@ 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)
- }
-
- if (date < now) {
+ while (date < now) {
date.setMonth(date.getMonth() + 1)
}
+
+ if (date.getDate() !== day) {
+ date.setDate(day)
+ }
return {
foundText: results[0],
diff --git a/src/i18n/lang/cs-CZ.json b/src/i18n/lang/cs-CZ.json
index ad67dd81..85dac7e1 100644
--- a/src/i18n/lang/cs-CZ.json
+++ b/src/i18n/lang/cs-CZ.json
@@ -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.",
diff --git a/src/i18n/lang/de-DE.json b/src/i18n/lang/de-DE.json
index 44ca9c0b..49543c4c 100644
--- a/src/i18n/lang/de-DE.json
+++ b/src/i18n/lang/de-DE.json
@@ -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.",
diff --git a/src/i18n/lang/de-swiss.json b/src/i18n/lang/de-swiss.json
index 7bb3dafe..76d8f042 100644
--- a/src/i18n/lang/de-swiss.json
+++ b/src/i18n/lang/de-swiss.json
@@ -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.",
diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json
index a6c86d72..1121d91b 100644
--- a/src/i18n/lang/en.json
+++ b/src/i18n/lang/en.json
@@ -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.",
diff --git a/src/i18n/lang/es-ES.json b/src/i18n/lang/es-ES.json
index 29537fe3..1f03d175 100644
--- a/src/i18n/lang/es-ES.json
+++ b/src/i18n/lang/es-ES.json
@@ -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.",
diff --git a/src/i18n/lang/fr-FR.json b/src/i18n/lang/fr-FR.json
index 1e566213..eae7cd95 100644
--- a/src/i18n/lang/fr-FR.json
+++ b/src/i18n/lang/fr-FR.json
@@ -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.",
diff --git a/src/i18n/lang/it-IT.json b/src/i18n/lang/it-IT.json
index 355d8ed5..647497b4 100644
--- a/src/i18n/lang/it-IT.json
+++ b/src/i18n/lang/it-IT.json
@@ -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}"
}
}
diff --git a/src/i18n/lang/nl-NL.json b/src/i18n/lang/nl-NL.json
index 29537fe3..1f03d175 100644
--- a/src/i18n/lang/nl-NL.json
+++ b/src/i18n/lang/nl-NL.json
@@ -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.",
diff --git a/src/i18n/lang/pt-BR.json b/src/i18n/lang/pt-BR.json
index 29537fe3..1f03d175 100644
--- a/src/i18n/lang/pt-BR.json
+++ b/src/i18n/lang/pt-BR.json
@@ -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.",
diff --git a/src/i18n/lang/pt-PT.json b/src/i18n/lang/pt-PT.json
index 29537fe3..1f03d175 100644
--- a/src/i18n/lang/pt-PT.json
+++ b/src/i18n/lang/pt-PT.json
@@ -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.",
diff --git a/src/i18n/lang/ro-RO.json b/src/i18n/lang/ro-RO.json
index 29537fe3..1f03d175 100644
--- a/src/i18n/lang/ro-RO.json
+++ b/src/i18n/lang/ro-RO.json
@@ -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.",
diff --git a/src/i18n/lang/ru-RU.json b/src/i18n/lang/ru-RU.json
index d5d98771..49b46546 100644
--- a/src/i18n/lang/ru-RU.json
+++ b/src/i18n/lang/ru-RU.json
@@ -899,7 +899,7 @@
"4015": "Комментарий не существует.",
"4016": "Неверное поле задачи.",
"4017": "Неверный сравнитель фильтров задач.",
- "4018": "Неверный соединитель фильтров задач.",
+ "4018": "Invalid task filter concatenator.",
"4019": "Неверное значение фильтра задач.",
"5001": "Пространство имён не существует.",
"5003": "Нет доступа к указанному пространству имён.",
diff --git a/src/i18n/lang/sv-SE.json b/src/i18n/lang/sv-SE.json
index 29537fe3..1f03d175 100644
--- a/src/i18n/lang/sv-SE.json
+++ b/src/i18n/lang/sv-SE.json
@@ -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.",
diff --git a/src/i18n/lang/tr-TR.json b/src/i18n/lang/tr-TR.json
index 29537fe3..1f03d175 100644
--- a/src/i18n/lang/tr-TR.json
+++ b/src/i18n/lang/tr-TR.json
@@ -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.",
diff --git a/src/i18n/lang/vi-VN.json b/src/i18n/lang/vi-VN.json
index 0b68ee80..d027aae7 100644
--- a/src/i18n/lang/vi-VN.json
+++ b/src/i18n/lang/vi-VN.json
@@ -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.",
diff --git a/src/icons.js b/src/icons.js
index 48ef230c..89e1d1a3 100644
--- a/src/icons.js
+++ b/src/icons.js
@@ -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)
diff --git a/src/main.ts b/src/main.ts
index 45108a74..b4ed880a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -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,
diff --git a/src/models/task.js b/src/models/task.js
index c0334237..63839689 100644
--- a/src/models/task.js
+++ b/src/models/task.js
@@ -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)
diff --git a/src/modules/parseTaskText.test.js b/src/modules/parseTaskText.test.js
index 19f3ba17..30e5743c 100644
--- a/src/modules/parseTaskText.test.js
+++ b/src/modules/parseTaskText.test.js
@@ -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')
})
@@ -32,7 +40,7 @@ describe('Parse Task Text', () => {
expect(result.assignees).toHaveLength(1)
expect(result.assignees[0]).toBe('user')
})
-
+
it('should ignore email addresses', () => {
const text = 'Lorem Ipsum email@example.com'
const result = parseTaskText(text)
@@ -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', () => {
diff --git a/src/router/index.ts b/src/router/index.ts
index 7b28a509..6e4ccce2 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -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: [
- {
- 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,
- },
- ],
- },
- {
- 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,
- },
- ],
- },
- {
- 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,
- },
- ],
- },
- {
- 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,
- },
- ],
- },
- ],
+ 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: 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: 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: 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: 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']
diff --git a/src/store/index.js b/src/store/index.js
index 37c74ae8..0833e3f6 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -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)
},
},
})
diff --git a/src/store/modules/auth.js b/src/store/modules/auth.js
index 42078322..e71bbe73 100644
--- a/src/store/modules/auth.js
+++ b/src/store/modules/auth.js
@@ -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: {
diff --git a/src/store/modules/config.js b/src/store/modules/config.js
index 82c2b20c..dfe1e01f 100644
--- a/src/store/modules/config.js
+++ b/src/store/modules/config.js
@@ -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])
- }
- },
},
}
\ No newline at end of file
diff --git a/src/store/modules/namespaces.js b/src/store/modules/namespaces.js
index ae47a95f..4512fb89 100644
--- a/src/store/modules/namespaces.js
+++ b/src/store/modules/namespaces.js
@@ -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')
}
},
diff --git a/src/store/modules/tasks.js b/src/store/modules/tasks.js
index 8d29cc89..504554c9 100644
--- a/src/store/modules/tasks.js
+++ b/src/store/modules/tasks.js
@@ -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
},
diff --git a/src/styles/common-imports.scss b/src/styles/common-imports.scss
index 5ca35fc4..a9e163e3 100644
--- a/src/styles/common-imports.scss
+++ b/src/styles/common-imports.scss
@@ -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;
diff --git a/src/styles/components/_index.scss b/src/styles/components/_index.scss
index ba87006b..080c93d0 100644
--- a/src/styles/components/_index.scss
+++ b/src/styles/components/_index.scss
@@ -2,4 +2,4 @@
@import "labels";
@import "list";
@import "task";
-@import "tasks";
\ No newline at end of file
+@import "tasks";
diff --git a/src/styles/custom-properties/colors.scss b/src/styles/custom-properties/colors.scss
index 2fc3ba0d..ae68dd60 100644
--- a/src/styles/custom-properties/colors.scss
+++ b/src/styles/custom-properties/colors.scss
@@ -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%;
}
}
\ No newline at end of file
diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss
new file mode 100644
index 00000000..687355e7
--- /dev/null
+++ b/src/styles/mixins.scss
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/styles/theme/background.scss b/src/styles/theme/background.scss
index b03b04f4..d478aac4 100644
--- a/src/styles/theme/background.scss
+++ b/src/styles/theme/background.scss
@@ -14,7 +14,7 @@
.box,
.card,
.switch-view,
- .table-view .button,
+ .list-table .button,
.filter-container .button,
.search .button {
box-shadow: none;
diff --git a/src/types/faker.d.ts b/src/types/faker.d.ts
new file mode 100644
index 00000000..de01ac25
--- /dev/null
+++ b/src/types/faker.d.ts
@@ -0,0 +1,4 @@
+declare module '@faker-js/faker' {
+ import faker from 'faker'
+ export default faker
+}
\ No newline at end of file
diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts
index 4f0571df..c04ff091 100644
--- a/src/types/shims-vue.d.ts
+++ b/src/types/shims-vue.d.ts
@@ -1,8 +1,11 @@
declare module 'vue' {
import { CompatVue } from '@vue/runtime-dom'
const Vue: CompatVue
- export default Vue
- export * from '@vue/runtime-dom'
+ 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
diff --git a/src/types/vue-flatpickr-component.d.ts b/src/types/vue-flatpickr-component.d.ts
new file mode 100644
index 00000000..2ac9cc87
--- /dev/null
+++ b/src/types/vue-flatpickr-component.d.ts
@@ -0,0 +1 @@
+declare module 'vue-flatpickr-component';
\ No newline at end of file
diff --git a/src/views/Home.vue b/src/views/Home.vue
index f3d2b3ec..14c160d4 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -23,7 +23,7 @@
{{ $t('home.list.newText') }}
diff --git a/src/views/list/views/Gantt.vue b/src/views/list/ListGantt.vue
similarity index 66%
rename from src/views/list/views/Gantt.vue
rename to src/views/list/ListGantt.vue
index b3628238..82b765dd 100644
--- a/src/views/list/views/Gantt.vue
+++ b/src/views/list/ListGantt.vue
@@ -1,6 +1,6 @@
-
-
+
+
{{ $t('list.gantt.showTasksWithoutDates') }}
@@ -44,65 +44,64 @@
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
-
\ No newline at end of file
diff --git a/src/views/list/views/List.vue b/src/views/list/ListList.vue
similarity index 79%
rename from src/views/list/views/List.vue
rename to src/views/list/ListList.vue
index d61a39e8..bb9e9478 100644
--- a/src/views/list/views/List.vue
+++ b/src/views/list/ListList.vue
@@ -1,8 +1,6 @@
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/views/list/ListWrapper.vue b/src/views/list/ListWrapper.vue
new file mode 100644
index 00000000..d816b90e
--- /dev/null
+++ b/src/views/list/ListWrapper.vue
@@ -0,0 +1,186 @@
+
+
+
+
+
+ {{ $t('list.list.title') }}
+
+
+ {{ $t('list.gantt.title') }}
+
+
+ {{ $t('list.table.title') }}
+
+
+ {{ $t('list.kanban.title') }}
+
+
+
+
+
+
+ {{ $t('list.archived') }}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/views/list/NewList.vue b/src/views/list/NewList.vue
index fb48e42b..0317a88e 100644
--- a/src/views/list/NewList.vue
+++ b/src/views/list/NewList.vue
@@ -61,7 +61,7 @@ export default {
}
this.showError = false
- this.list.namespaceId = parseInt(this.$route.params.id)
+ this.list.namespaceId = parseInt(this.$route.params.namespaceId)
const list = await this.$store.dispatch('lists/createList', this.list)
this.$message.success({message: this.$t('list.create.createdSuccess') })
this.$router.push({
diff --git a/src/views/list/ShowList.vue b/src/views/list/ShowList.vue
deleted file mode 100644
index d0c1d20f..00000000
--- a/src/views/list/ShowList.vue
+++ /dev/null
@@ -1,211 +0,0 @@
-
-
-
-
-
- {{ $t('list.list.title') }}
-
-
- {{ $t('list.gantt.title') }}
-
-
- {{ $t('list.table.title') }}
-
-
- {{ $t('list.kanban.title') }}
-
-
-
-
-
- {{ $t('list.archived') }}
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/views/list/settings/share.vue b/src/views/list/settings/share.vue
index 4a9967f8..ca11b4e5 100644
--- a/src/views/list/settings/share.vue
+++ b/src/views/list/settings/share.vue
@@ -3,24 +3,38 @@
:title="$t('list.share.header')"
primary-label=""
>
-
-
+
+
+
+
-
+
-
+
+
diff --git a/src/views/list/views/Table.vue b/src/views/list/views/Table.vue
deleted file mode 100644
index b15b0a1b..00000000
--- a/src/views/list/views/Table.vue
+++ /dev/null
@@ -1,331 +0,0 @@
-
-
-
-
-
-
-
- {{ $t('list.table.columns') }}
-
-
-
-
- #
-
- {{ $t('task.attributes.done') }}
-
-
- {{ $t('task.attributes.title') }}
-
-
- {{ $t('task.attributes.priority') }}
-
-
- {{ $t('task.attributes.labels') }}
-
-
- {{ $t('task.attributes.assignees') }}
-
-
- {{ $t('task.attributes.dueDate') }}
-
-
- {{ $t('task.attributes.startDate') }}
-
-
- {{ $t('task.attributes.endDate') }}
-
-
- {{ $t('task.attributes.percentDone') }}
-
-
- {{ $t('task.attributes.created') }}
-
-
- {{ $t('task.attributes.updated') }}
-
-
- {{ $t('task.attributes.createdBy') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #
-
-
-
- {{ $t('task.attributes.done') }}
-
-
-
- {{ $t('task.attributes.title') }}
-
-
-
- {{ $t('task.attributes.priority') }}
-
-
-
- {{ $t('task.attributes.labels') }}
-
-
- {{ $t('task.attributes.assignees') }}
-
-
- {{ $t('task.attributes.dueDate') }}
-
-
-
- {{ $t('task.attributes.startDate') }}
-
-
-
- {{ $t('task.attributes.endDate') }}
-
-
-
- {{ $t('task.attributes.percentDone') }}
-
-
-
- {{ $t('task.attributes.created') }}
-
-
-
- {{ $t('task.attributes.updated') }}
-
-
-
- {{ $t('task.attributes.createdBy') }}
-
-
-
-
-
-
-
-
- #{{ t.index }}
-
-
- {{ t.identifier }}
-
-
-
-
-
-
-
- {{ t.title }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t.percentDone * 100 }}%
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/views/migrator/MigrateService.vue b/src/views/migrator/MigrateService.vue
index 1f3573eb..c77e2aa5 100644
--- a/src/views/migrator/MigrateService.vue
+++ b/src/views/migrator/MigrateService.vue
@@ -254,4 +254,13 @@ export default {
background-color: var(--primary-dark);
}
}
+
+@media (prefers-reduced-motion: reduce) {
+ @keyframes wave {
+ 10% {
+ transform: translate(0, 0);
+ background-color: var(--primary);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/views/namespaces/ListNamespaces.vue b/src/views/namespaces/ListNamespaces.vue
index fdefe4f9..0a63b050 100644
--- a/src/views/namespaces/ListNamespaces.vue
+++ b/src/views/namespaces/ListNamespaces.vue
@@ -22,9 +22,9 @@
-
+
{{ $t('namespace.noLists') }}
-
+
{{ $t('namespace.createList') }}
@@ -64,7 +64,7 @@
:show-archived="showArchived"
/>
-
+
diff --git a/src/views/namespaces/settings/archive.vue b/src/views/namespaces/settings/archive.vue
index 9eb1305b..e08457e9 100644
--- a/src/views/namespaces/settings/archive.vue
+++ b/src/views/namespaces/settings/archive.vue
@@ -4,9 +4,11 @@
@submit="archiveNamespace()"
>
{{ title }}
-
+
- {{ list.isArchived ? $t('namespace.archive.unarchiveText') : $t('namespace.archive.archiveText') }}
+
+ {{ namespace.isArchived ? $t('namespace.archive.unarchiveText') : $t('namespace.archive.archiveText')}}
+
@@ -27,17 +29,18 @@ export default {
created() {
this.namespace = this.$store.getters['namespaces/getNamespaceById'](this.$route.params.id)
this.title = this.namespace.isArchived ?
- this.$t('namespace.archive.titleUnarchive', { namespace: this.namespace.title }) :
- this.$t('namespace.archive.titleArchive', { namespace: this.namespace.title })
+ this.$t('namespace.archive.titleUnarchive', {namespace: this.namespace.title}) :
+ this.$t('namespace.archive.titleArchive', {namespace: this.namespace.title})
this.setTitle(this.title)
},
methods: {
async archiveNamespace() {
- this.namespace.isArchived = !this.namespace.isArchived
-
try {
- const namespace = await this.namespaceService.update(this.namespace)
+ const namespace = await this.namespaceService.update({
+ ...this.namespace,
+ isArchived: !this.namespace.isArchived,
+ })
this.$store.commit('namespaces/setNamespaceById', namespace)
this.$message.success({message: this.$t('namespace.archive.success')})
} finally {
diff --git a/src/views/namespaces/settings/share.vue b/src/views/namespaces/settings/share.vue
index 8f70cbd5..f3e4a20c 100644
--- a/src/views/namespaces/settings/share.vue
+++ b/src/views/namespaces/settings/share.vue
@@ -3,69 +3,67 @@
:title="title"
primary-label=""
>
-
-
+
+
+
+
-
+
+
\ No newline at end of file
diff --git a/src/views/sharing/LinkSharingAuth.vue b/src/views/sharing/LinkSharingAuth.vue
index 41a66739..66744930 100644
--- a/src/views/sharing/LinkSharingAuth.vue
+++ b/src/views/sharing/LinkSharingAuth.vue
@@ -44,7 +44,7 @@ import Message from '@/components/misc/message.vue'
const {t} = useI18n()
useTitle(t('sharing.authenticating'))
-async function useAuth() {
+function useAuth() {
const store = useStore()
const route = useRoute()
const router = useRouter()
@@ -75,21 +75,21 @@ async function useAuth() {
password: password.value,
})
router.push({name: 'list.list', params: {listId}})
- } catch (e) {
+ } catch (e: any) {
if (e.response?.data?.code === 13001) {
authenticateWithPassword.value = true
return
}
// TODO: Put this logic in a global errorMessage handler method which checks all auth codes
- let errorMessage = t('sharing.error')
+ let err = t('sharing.error')
if (e.response?.data?.message) {
- errorMessage = e.response.data.message
+ err = e.response.data.message
}
if (e.response?.data?.code === 13002) {
- errorMessage = t('sharing.invalidPassword')
+ err = t('sharing.invalidPassword')
}
- errorMessage.value = errorMessage
+ errorMessage.value = err
} finally {
loading.value = false
}
diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue
index e2171698..ed1c0eb2 100644
--- a/src/views/tasks/TaskDetailView.vue
+++ b/src/views/tasks/TaskDetailView.vue
@@ -263,6 +263,7 @@
{{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }}
-
- {{ formatDateSince(task.created) }}
- {{ task.createdBy.getDisplayName() }}
-
+
+
+ {{ formatDateSince(task.created) }}
+ {{ task.createdBy.getDisplayName() }}
+
+
-
- {{ updatedSince }}
-
+
+
+ {{ updatedSince }}
+
+
-
- {{ doneSince }}
-
+
+
+ {{ doneSince }}
+
+
@@ -453,8 +460,10 @@ import {CURRENT_LIST} from '@/store/mutation-types'
import {uploadFile} from '@/helpers/attachments'
import ChecklistSummary from '../../components/tasks/partials/checklist-summary'
+
export default {
name: 'TaskDetailView',
+ compatConfig: { ATTR_FALSE_VALUE: false },
components: {
ChecklistSummary,
TaskSubscription,
@@ -473,6 +482,14 @@ export default {
description,
heading,
},
+
+ props: {
+ taskId: {
+ type: Number,
+ required: true,
+ },
+ },
+
data() {
return {
taskService: new TaskService(),
@@ -523,10 +540,6 @@ export default {
},
},
computed: {
- taskId() {
- const {id} = this.$route.params
- return id === undefined ? id : Number(id)
- },
currentList() {
return this.$store.state[CURRENT_LIST]
},
@@ -589,6 +602,9 @@ export default {
}
},
scrollToHeading() {
+ if(!this.$refs?.heading?.$el) {
+ return
+ }
this.$refs.heading.$el.scrollIntoView({block: 'center'})
},
setActiveFields() {
@@ -931,4 +947,14 @@ $flash-background-duration: 750ms;
background: transparent;
}
}
+
+@media (prefers-reduced-motion: reduce) {
+ @keyframes flash-background {
+ 0% {
+ background: transparent;
+ }
+ }
+}
+
+@include modal-transition();
\ No newline at end of file
diff --git a/src/views/tasks/TaskDetailViewModal.vue b/src/views/tasks/TaskDetailViewModal.vue
deleted file mode 100644
index 9b493246..00000000
--- a/src/views/tasks/TaskDetailViewModal.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/views/teams/EditTeam.vue b/src/views/teams/EditTeam.vue
index 059530e8..31c690f9 100644
--- a/src/views/teams/EditTeam.vue
+++ b/src/views/teams/EditTeam.vue
@@ -308,4 +308,6 @@ export default {
padding: 0;
}
}
+
+@include modal-transition();
\ No newline at end of file
diff --git a/src/views/user/Login.vue b/src/views/user/Login.vue
index d04b9736..b6e5be03 100644
--- a/src/views/user/Login.vue
+++ b/src/views/user/Login.vue
@@ -1,9 +1,9 @@
-
+
{{ $t('user.auth.confirmEmailSuccess') }}
-
+
{{ errorMessage }}
+
+ {{ $t('user.auth.usernameRequired') }}
+
-
{{ $t('user.auth.password') }}
-
-
+
+ {{ $t('user.auth.password') }}
+
+ {{ $t('user.auth.forgotPassword') }}
+
+
{{ $t('user.auth.totpTitle') }}
@@ -52,32 +54,28 @@
type="text"
v-focus
@keyup.enter="submit"
+ tabindex="3"
/>
-
+
+ {{ $t('user.auth.login') }}
+
+
+ {{ $t('user.auth.noAccountYet') }}
+
+ {{ $t('user.auth.createAccount') }}
+
+
diff --git a/src/views/user/PasswordReset.vue b/src/views/user/PasswordReset.vue
index a721f84d..0fdca381 100644
--- a/src/views/user/PasswordReset.vue
+++ b/src/views/user/PasswordReset.vue
@@ -1,9 +1,9 @@
-
+
{{ errorMsg }}
-
+
{{ successMessage }}
diff --git a/src/views/user/Register.vue b/src/views/user/Register.vue
index 3f13b23c..ce95eeae 100644
--- a/src/views/user/Register.vue
+++ b/src/views/user/Register.vue
@@ -1,6 +1,6 @@
-
+
{{ errorMessage }}
+
+ {{ $t('user.auth.usernameRequired') }}
+
{{ $t('user.auth.email') }}
@@ -33,68 +37,46 @@
type="email"
v-model="credentials.email"
@keyup.enter="submit"
+ @focusout="validateEmail"
/>
+
+ {{ $t('user.auth.emailInvalid') }}
+
{{ $t('user.auth.password') }}
-
-
-
-
-
-
{{ $t('user.auth.passwordRepeat') }}
-
-
-
+
credentials.password = v" :validate-initially="validatePasswordInitially"/>
-
-
-
- {{ $t('user.auth.register') }}
-
-
- {{ $t('user.auth.login') }}
-
-
-
+
+ {{ $t('user.auth.createAccount') }}
+
+
+ {{ $t('user.auth.alreadyHaveAnAccount') }}
+
+ {{ $t('user.auth.login') }}
+
+