Compare commits

..

9 Commits

Author SHA1 Message Date
e79715faa8
Merge branch 'main' into feature/blur-hash 2022-01-16 18:35:26 +01:00
bfb356eb8b
Merge branch 'main' into feature/blur-hash
# Conflicts:
#	package.json
#	src/components/home/contentAuth.vue
2022-01-08 13:51:36 +01:00
b85df38245
fix: lint 2021-12-20 22:08:18 +01:00
33158167d5
chore: make background fade in reusable 2021-12-20 22:06:45 +01:00
0e886ba76b
feat: add blurHash loading for list cards 2021-12-20 22:04:00 +01:00
daf3212902
feat: add fade in for background images 2021-12-20 21:36:33 +01:00
af177071d6
Merge branch 'main' into feature/blur-hash
# Conflicts:
#	package.json
2021-12-20 19:42:55 +01:00
b5f693e230
feat: use blurHash when loading list backgrounds 2021-12-12 22:31:51 +01:00
356e01cd14
feat: use BlurHash when rendering unsplash search results 2021-12-12 22:25:11 +01:00
141 changed files with 5238 additions and 6657 deletions

View File

@ -116,16 +116,36 @@ 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 --record
- yarn test:frontend --browser chrome
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
@ -645,6 +665,6 @@ steps:
from_secret: crowdin_key
---
kind: signature
hmac: 997e1badebe484ac29557c4af356e63db4d3d57f3d32e92d482f117f8cec64da
hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de
...

View File

@ -7,6 +7,5 @@
"video": false,
"retries": {
"runMode": 2
},
"projectId": "181c7x"
}
}

View File

@ -1,4 +1,4 @@
import faker from '@faker-js/faker'
import faker from 'faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'

View File

@ -1,4 +1,4 @@
import faker from '@faker-js/faker'
import faker from 'faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'

View File

@ -1,6 +1,6 @@
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
import faker from '@faker-js/faker'
import faker from 'faker'
export class LinkShareFactory extends Factory {
static table = 'link_shares'

View File

@ -1,6 +1,6 @@
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"
import faker from '@faker-js/faker'
import faker from 'faker'
export class ListFactory extends Factory {
static table = 'lists'

View File

@ -1,4 +1,4 @@
import faker from '@faker-js/faker'
import faker from 'faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'

View File

@ -1,4 +1,4 @@
import faker from '@faker-js/faker'
import faker from 'faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'

View File

@ -1,4 +1,4 @@
import faker from '@faker-js/faker'
import faker from 'faker'
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"

View File

@ -1,4 +1,4 @@
import faker from '@faker-js/faker'
import faker from 'faker'
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'

View File

@ -1,4 +1,4 @@
import faker from '@faker-js/faker'
import faker from 'faker'
import {Factory} from '../support/factory'
import {formatISO} from "date-fns"

View File

@ -1,56 +0,0 @@
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)
})
})

View File

@ -1,76 +0,0 @@
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})
})
})

View File

@ -1,196 +0,0 @@
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')
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)
})
})

View File

@ -1,97 +0,0 @@
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)
})
})

View File

@ -1,52 +0,0 @@
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}`)
})
})

View File

@ -1,11 +1,25 @@
import {formatISO, format} from 'date-fns'
import {TaskFactory} from '../../factories/task'
import {prepareLists} from './prepareLists'
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 '../../support/authenticateUser'
describe('Lists', () => {
let lists
prepareLists((newLists) => (lists = newLists))
beforeEach(() => {
UserFactory.create(1)
NamespaceFactory.create(1)
lists = ListFactory.create(1, {
title: 'First List'
})
TaskFactory.truncate()
})
it('Should create a new list', () => {
cy.visit('/')
@ -15,7 +29,7 @@ describe('Lists', () => {
.contains('New list')
.click()
cy.url()
.should('contain', '/lists/new/1')
.should('contain', '/namespaces/1/list')
cy.get('.card-header-title')
.contains('New list')
cy.get('input.input')
@ -42,7 +56,7 @@ describe('Lists', () => {
})
it('Should rename the list in all places', () => {
TaskFactory.create(5, {
const tasks = TaskFactory.create(5, {
id: '{increment}',
list_id: 1,
})
@ -98,4 +112,429 @@ 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)
})
})
})

View File

@ -1,16 +0,0 @@
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()
})
}

View File

@ -116,7 +116,6 @@ describe('Task', () => {
.should('be.visible')
.should('contain', 'Done')
cy.get('.task-view .action-buttons p.created')
.scrollIntoView()
.should('be.visible')
.should('contain', 'Done')
})
@ -373,13 +372,13 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`)
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
.should('be.visible')
.should('contain', labels[0].title)
cy.getSettled('.task-view .details.labels-list .multiselect .input-wrapper')
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
.children()
.first()
.get('[data-cy="taskDetail.removeLabel"]')
.get('a.delete')
.click()
cy.get('.global-notification')

View File

@ -6,7 +6,7 @@ describe('Log out', () => {
cy.get('.navbar .user .username')
.click()
cy.get('.navbar .user .dropdown-menu .dropdown-item')
cy.get('.navbar .user .dropdown-menu a.dropdown-item')
.contains('Logout')
.click()

View File

@ -25,6 +25,7 @@ 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
@ -42,6 +43,7 @@ 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.')
})

View File

@ -8,14 +8,12 @@ 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})
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
cy.get('input[type=file]', { timeout: 1000 })
.attachFile('image.jpg')
cy.get('.vue-handler-wrapper.vue-handler-wrapper--south .vue-simple-handler.vue-simple-handler--south')
.trigger('mousedown', {which: 1})
.trigger('mousemove', {clientY: 100})
@ -24,7 +22,7 @@ describe('User Settings', () => {
.contains('Upload Avatar')
.click()
cy.wait('@uploadAvatar')
cy.wait(3000) // Wait for the request to finish
cy.get('.global-notification')
.should('contain', 'Success')
})

View File

@ -1,5 +1,6 @@
import './commands'
import 'cypress-file-upload'
import '@4tw/cypress-drag-drop'
// see https://github.com/cypress-io/cypress/issues/702#issuecomment-587127275

View File

@ -18,20 +18,21 @@
"browserslist:update": "npx browserslist@latest --update-db"
},
"dependencies": {
"@github/hotkey": "2.0.0",
"@github/hotkey": "1.6.1",
"@kyvg/vue3-notification": "2.3.4",
"@sentry/tracing": "6.17.7",
"@sentry/vue": "6.17.7",
"@sentry/tracing": "6.16.1",
"@sentry/vue": "6.16.1",
"@types/is-touch-device": "1.0.0",
"@vue/compat": "3.2.30",
"@vueuse/core": "7.6.0",
"@vueuse/router": "7.6.1",
"@vue/compat": "3.2.27",
"@vueuse/core": "7.5.2",
"@vueuse/router": "7.5.3",
"blurhash": "^1.1.4",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.1",
"codemirror": "5.65.0",
"copy-to-clipboard": "3.3.1",
"date-fns": "2.28.0",
"dompurify": "2.3.5",
"dompurify": "2.3.4",
"easymde": "2.16.1",
"flatpickr": "4.6.9",
"flexsearch": "0.7.21",
@ -39,16 +40,16 @@
"is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.0.12",
"marked": "4.0.10",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"ufo": "0.7.10",
"ufo": "0.7.9",
"v-tooltip": "4.0.0-beta.17",
"vue": "3.2.30",
"vue-advanced-cropper": "2.8.0",
"vue": "3.2.27",
"vue-advanced-cropper": "2.7.1",
"vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.5",
"vue-i18n": "9.2.0-beta.30",
"vue-i18n": "9.2.0-beta.28",
"vue-router": "4.0.12",
"vuedraggable": "4.1.0",
"vuex": "4.0.2",
@ -56,41 +57,42 @@
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.1.0",
"@faker-js/faker": "6.0.0-alpha.6",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@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.11.0",
"@typescript-eslint/parser": "5.11.0",
"@vitejs/plugin-legacy": "1.7.1",
"@vitejs/plugin-vue": "2.2.0",
"@typescript-eslint/eslint-plugin": "5.9.1",
"@typescript-eslint/parser": "5.9.1",
"@vitejs/plugin-legacy": "1.6.4",
"@vitejs/plugin-vue": "2.0.1",
"@vue/eslint-config-typescript": "10.0.0",
"autoprefixer": "10.4.2",
"axios": "0.25.0",
"axios": "0.24.0",
"browserslist": "4.19.1",
"caniuse-lite": "1.0.30001311",
"cypress": "9.4.1",
"esbuild": "0.14.21",
"eslint": "8.9.0",
"eslint-plugin-vue": "8.4.1",
"caniuse-lite": "1.0.30001299",
"cypress": "9.2.1",
"cypress-file-upload": "5.0.8",
"esbuild": "0.14.11",
"eslint": "8.7.0",
"eslint-plugin-vue": "8.3.0",
"express": "4.17.2",
"happy-dom": "2.31.1",
"netlify-cli": "8.16.1",
"postcss": "8.4.6",
"postcss-preset-env": "7.3.1",
"rollup": "2.67.2",
"faker": "5.5.3",
"netlify-cli": "8.8.2",
"happy-dom": "2.25.2",
"postcss": "8.4.5",
"postcss-preset-env": "7.2.3",
"rollup": "2.64.0",
"rollup-plugin-visualizer": "5.5.4",
"sass": "1.49.7",
"sass": "1.48.0",
"slugify": "1.6.5",
"typescript": "4.5.5",
"vite": "2.7.13",
"typescript": "4.5.4",
"vite": "2.7.12",
"vite-plugin-pwa": "0.11.13",
"vite-svg-loader": "3.1.2",
"vitest": "0.3.2",
"vue-tsc": "0.31.2",
"wait-on": "6.0.1",
"vitest": "0.1.17",
"vue-tsc": "0.30.4",
"wait-on": "6.0.0",
"workbox-cli": "6.4.2"
},
"eslintConfig": {
@ -129,7 +131,7 @@
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 2022
"ecmaVersion": 2021
},
"ignorePatterns": [
"*.test.*",

View File

@ -1,7 +1,7 @@
<template>
<ready>
<template v-if="authUser">
<TheNavigation/>
<top-navigation/>
<content-auth/>
</template>
<content-link-share v-else-if="authLinkShare"/>
@ -27,7 +27,7 @@ import {success} from '@/message'
import Notification from '@/components/misc/notification.vue'
import KeyboardShortcuts from './components/misc/keyboard-shortcuts/index.vue'
import TheNavigation from '@/components/home/TheNavigation.vue'
import TopNavigation from './components/home/topNavigation.vue'
import ContentAuth from './components/home/contentAuth.vue'
import ContentLinkShare from './components/home/contentLinkShare.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.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'])

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -69,10 +69,10 @@ watchEffect(() => {
}
// if there is a href we assume the user wants an external link via a link element
// we also set a predefined value for the attribute rel, but make it possible to overwrite this by the user.
// we also set the attribute rel to "noopener" but make it possible to overwrite this by the user.
if ('href' in attrs) {
nodeName = 'a'
bindings = {rel: 'noreferrer noopener nofollow'}
bindings = {rel: 'noopener'}
}
componentNodeName.value = nodeName

View File

@ -1,12 +1,9 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useNow } from '@vueuse/core'
import LogoFull from '@/assets/logo-full.svg?component'
import LogoFullPride from '@/assets/logo-full-pride.svg?component'
const now = useNow()
const Logo = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
const Logo = computed(() => new Date().getMonth() === 5 ? LogoFullPride : LogoFull)
</script>
<template>

View File

@ -1,7 +1,8 @@
<template>
<BaseButton
class="menu-show-button"
<button
type="button"
@click="$store.commit('toggleMenu')"
class="menu-show-button"
@shortkey="() => $store.commit('toggleMenu')"
v-shortcut="'Control+e'"
:title="$t('keyboardShortcuts.toggleMenu')"
@ -9,14 +10,11 @@
/>
</template>
<script setup lang="ts">
<script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'
import {store} from '@/store'
import BaseButton from '@/components/base/BaseButton.vue'
const store = useStore()
const menuActive = computed(() => store.state.menuActive)
const menuActive = computed(() => store.menuActive)
</script>
<style lang="scss" scoped>
@ -24,6 +22,11 @@ $lineWidth: 2rem;
$size: $lineWidth + 1rem;
.menu-show-button {
// FIXME: create general button component
appearance: none;
background-color: transparent;
border: 0;
min-height: $size;
width: $size;

View File

@ -1,51 +1,37 @@
<template>
<div>
<BaseButton
v-if="menuActive"
@click="$store.commit('menuActive', false)"
class="menu-hide-button"
>
<icon icon="times" />
</BaseButton>
<a @click="$store.commit('menuActive', false)" class="menu-hide-button" v-if="menuActive">
<icon icon="times"/>
</a>
<div
:class="{'has-background': background}"
:style="{'background-image': background && `url(${background})`}"
:class="{'has-background': background || blurHash}"
:style="{'background-image': blurHash && `url(${blurHash})`}"
class="app-container"
>
<div
:class="{'is-visible': background}"
class="app-container-background background-fade-in"
:style="{'background-image': background && `url(${background})`}"></div>
<navigation/>
<main
<div
:class="[
{ 'is-menu-enabled': menuActive },
$route.name,
]"
class="app-content"
>
<BaseButton
v-if="menuActive"
@click="$store.commit('menuActive', false)"
class="mobile-overlay"
/>
<a @click="$store.commit('menuActive', false)" class="mobile-overlay" v-if="menuActive"></a>
<quick-actions/>
<router-view/>
<router-view :route="routeWithModal" v-slot="{ Component }">
<keep-alive :include="['list.list', 'list.gantt', 'list.table', 'list.kanban']">
<component :is="Component" />
</keep-alive>
<router-view name="popup" v-slot="{ Component }">
<transition name="modal">
<component :is="Component"/>
</transition>
</router-view>
<transition name="modal">
<modal
v-if="currentModal"
@close="closeModal()"
variant="scrolling"
class="task-detail-view-modal"
>
<component :is="currentModal" />
</modal>
</transition>
<a
class="keyboard-shortcuts-button"
@click="showKeyboardShortcuts()"
@ -53,13 +39,13 @@
>
<icon icon="keyboard"/>
</a>
</main>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {watch, computed, shallowRef, watchEffect, VNode, h} from 'vue'
import {watch, computed} from 'vue'
import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
@ -67,63 +53,11 @@ import {useEventListener} from '@vueuse/core'
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue'
import BaseButton from '@/components/base/BaseButton.vue'
function useRouteWithModal() {
const router = useRouter()
const route = useRoute()
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
const routeWithModal = computed(() => {
return backdropView.value
? router.resolve(backdropView.value)
: route
})
const currentModal = shallowRef<VNode>()
watchEffect(() => {
if (!backdropView.value) {
currentModal.value = undefined
return
}
// logic from vue-router
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
const routePropsOption = route.matched[0]?.props.default
const routeProps = routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
currentModal.value = h(
route.matched[0]?.components.default,
routeProps,
)
})
function closeModal() {
const historyState = computed(() => route.fullPath && window.history.state)
if (historyState.value) {
router.back()
} else {
const backdropRoute = historyState.value?.backdropView && router.resolve(historyState.value.backdropView)
router.push(backdropRoute)
}
}
return { routeWithModal, currentModal, closeModal }
}
const { routeWithModal, currentModal, closeModal } = useRouteWithModal()
const store = useStore()
const background = computed(() => store.state.background)
const blurHash = computed(() => store.state.blurHash)
const menuActive = computed(() => store.state.menuActive)
function showKeyboardShortcuts() {
@ -151,7 +85,7 @@ watch(() => route.name as string, (routeName) => {
'migrate.start',
'migrate.wunderlist',
'namespaces.index',
].includes(routeName) ||
].includes(routeName) ||
routeName.startsWith('user.settings')
)
) {
@ -163,7 +97,7 @@ watch(() => route.name as string, (routeName) => {
function useRenewTokenOnFocus() {
const router = useRouter()
const userInfo = computed(() => store.state.auth.info)
const authenticated = computed(() => store.state.auth.authenticated)
@ -227,40 +161,41 @@ store.dispatch('labels/loadAllLabels')
}
.app-container {
min-height: calc(100vh - 65px);
min-height: calc(100vh - 65px);
@media screen and (max-width: $tablet) {
padding-top: $navbar-height;
}
.app-content {
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
z-index: 2;
@media screen and (max-width: $tablet) {
margin-left: 0;
padding-top: 1.5rem;
min-height: calc(100vh - 4rem);
}
&.is-menu-enabled {
margin-left: $navbar-width;
@media screen and (max-width: $tablet) {
min-width: 100%;
margin-left: 0;
}
}
&.task\.detail {
padding-left: 0;
padding-right: 0;
@media screen and (max-width: $tablet) {
padding-top: $navbar-height;
}
.card {
background: var(--white);
}
}
.app-content {
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
z-index: 10;
position: relative;
@media screen and (max-width: $tablet) {
margin-left: 0;
padding-top: 1.5rem;
min-height: calc(100vh - 4rem);
}
&.is-menu-enabled {
margin-left: $navbar-width;
@media screen and (max-width: $tablet) {
min-width: 100%;
margin-left: 0;
}
}
&.task\.detail {
padding-left: 0;
padding-right: 0;
}
.card {
background: var(--white);
}
}
}
.mobile-overlay {
@ -289,11 +224,9 @@ store.dispatch('labels/loadAllLabels')
color: var(--grey-500);
transition: color $transition;
@media screen and (max-width: $tablet) {
display: none;
}
}
@include modal-transition();
</style>

View File

@ -1,8 +1,8 @@
<template>
<aside :class="{'is-active': menuActive}" class="namespace-container">
<nav class="menu top-menu">
<div :class="{'is-active': menuActive}" class="namespace-container">
<div class="menu top-menu">
<router-link :to="{name: 'home'}" class="logo">
<Logo width="164" height="48"/>
<Logo width="164" height="48" />
</router-link>
<ul class="menu-list">
<li>
@ -46,35 +46,31 @@
</router-link>
</li>
</ul>
</nav>
</div>
<nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
<template v-for="(n, nk) in namespaces" :key="n.id">
<aside class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
<template v-for="(n, nk) in namespaces" :key="n.id" >
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
<span
@click="toggleLists(n.id)"
class="menu-label"
v-tooltip="namespaceTitles[nk]"
>
<span
v-if="n.hexColor !== ''"
:style="{ backgroundColor: n.hexColor }"
class="color-bubble"
/>
v-tooltip="namespaceTitles[nk]">
<span class="name">
<span
:style="{ backgroundColor: n.hexColor }"
class="color-bubble"
v-if="n.hexColor !== ''">
</span>
{{ namespaceTitles[nk] }}
</span>
<a
class="icon is-small toggle-lists-icon pl-2"
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
@click="toggleLists(n.id)"
>
<icon icon="chevron-down"/>
</a>
<span class="count" :class="{'ml-2 mr-0': n.id > 0}">
({{ namespaceListsCount[nk] }})
</span>
</span>
<a
class="icon is-small toggle-lists-icon"
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
@click="toggleLists(n.id)"
>
<icon icon="chevron-down"/>
</a>
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
</div>
<div
@ -85,20 +81,18 @@
<!--
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
triggered by the change needs to have access to the current namespace
-->
-->
<draggable
v-bind="dragOptions"
:modelValue="activeLists[nk]"
@update:modelValue="(lists) => updateActiveLists(n, lists)"
group="namespace-lists"
:group="`namespace-${n.id}-lists`"
@start="() => drag = true"
@end="saveListPosition"
@end="e => saveListPosition(e, nk)"
handle=".handle"
:disabled="n.id < 0 || null"
tag="transition-group"
item-key="id"
:data-namespace-id="n.id"
:data-namespace-index="nk"
:component-data="{
type: 'transition',
tag: 'ul',
@ -140,7 +134,7 @@
:class="{'is-favorite': l.isFavorite}"
@click.prevent.stop="toggleFavoriteList(l)"
class="favorite">
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']" />
</span>
</a>
</router-link>
@ -151,9 +145,9 @@
</draggable>
</div>
</template>
</nav>
<PoweredByLink/>
</aside>
</aside>
<PoweredByLink />
</div>
</template>
<script>
@ -200,13 +194,13 @@ export default {
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
}),
activeLists() {
return this.namespaces.map(({lists}) => lists?.filter(item => typeof item !== 'undefined' && !item.isArchived))
return this.namespaces.map(({lists}) => lists?.filter(item => !item.isArchived))
},
namespaceTitles() {
return this.namespaces.map((namespace) => this.getNamespaceTitle(namespace))
},
namespaceListsCount() {
return this.namespaces.map((_, index) => this.activeLists[index]?.length ?? 0)
return this.namespaces.map((namespace, index) => {
const title = this.getNamespaceTitle(namespace)
return `${title} (${this.activeLists[index]?.length ?? 0})`
})
},
},
beforeCreate() {
@ -243,15 +237,15 @@ export default {
this.listsVisible[namespaceId] = !this.listsVisible[namespaceId]
},
updateActiveLists(namespace, activeLists) {
// This is a bit hacky: since we do have to filter out the archived items from the list
// this is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier.
const lists = [
...activeLists,
...namespace.lists.filter(l => l.isArchived),
]
// instead we iterate over the non archived items in the old list and replace them with the ones in their new order
const lists = namespace.lists.map((item) => {
if (item.isArchived) {
return item
}
return activeLists.shift()
})
const newNamespace = {
...namespace,
@ -261,11 +255,8 @@ export default {
this.$store.commit('namespaces/setNamespaceById', newNamespace)
},
async saveListPosition(e) {
const namespaceId = parseInt(e.to.dataset.namespaceId)
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex)
const listsActive = this.activeLists[newNamespaceIndex]
async saveListPosition(e, namespaceIndex) {
const listsActive = this.activeLists[namespaceIndex]
const list = listsActive[e.newIndex]
const listBefore = listsActive[e.newIndex - 1] ?? null
const listAfter = listsActive[e.newIndex + 1] ?? null
@ -278,7 +269,6 @@ export default {
await this.$store.dispatch('lists/updateList', {
...list,
position,
namespaceId,
})
} finally {
this.listUpdating[list.id] = false
@ -375,9 +365,8 @@ $vikunja-nav-selected-width: 0.4rem;
.menu-label {
.color-bubble {
width: 14px;
height: 14px;
flex-basis: auto;
width: 14px !important;
height: 14px !important;
}
.is-archived {
@ -398,12 +387,6 @@ $vikunja-nav-selected-width: 0.4rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: auto;
}
.count {
color: var(--grey-500);
margin-right: .5rem;
}
}
@ -499,7 +482,7 @@ $vikunja-nav-selected-width: 0.4rem;
height: 1rem;
vertical-align: middle;
padding-right: 0.5rem;
&.handle {
opacity: 0;
transition: opacity $transition;
@ -507,7 +490,7 @@ $vikunja-nav-selected-width: 0.4rem;
cursor: grab;
}
}
&:hover .icon.handle {
opacity: 1;
}
@ -559,7 +542,7 @@ $vikunja-nav-selected-width: 0.4rem;
span.list-menu-link, li > a {
padding-left: 2rem;
display: inline-block;
.icon {
padding-bottom: .25rem;
}

View File

@ -1,8 +1,9 @@
<template>
<header
<nav
:class="{'has-background': background}"
aria-label="main navigation"
class="navbar main-theme is-fixed-top"
role="navigation"
>
<router-link :to="{name: 'home'}" class="logo-link">
<Logo width="164" height="48"/>
@ -32,13 +33,12 @@
</a>
<notifications/>
<div class="user">
<img :src="userAvatar" alt="" class="avatar" width="40" height="40"/>
<dropdown class="is-right" ref="usernameDropdown">
<template #trigger>
<x-button
variant="secondary"
:shadow="false"
>
<img :src="userAvatar" alt="" class="avatar" width="40" height="40"/>
:shadow="false">
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
<span class="icon is-small">
<icon icon="chevron-down"/>
@ -46,96 +46,92 @@
</x-button>
</template>
<BaseButton
:to="{name: 'user.settings'}"
class="dropdown-item"
>
<router-link :to="{name: 'user.settings'}" class="dropdown-item">
{{ $t('user.settings.title') }}
</BaseButton>
<BaseButton
v-if="imprintUrl"
</router-link>
<a
:href="imprintUrl"
class="dropdown-item"
>
target="_blank"
rel="noreferrer noopener nofollow"
v-if="imprintUrl">
{{ $t('navigation.imprint') }}
</BaseButton>
<BaseButton
v-if="privacyPolicyUrl"
</a>
<a
:href="privacyPolicyUrl"
class="dropdown-item"
>
target="_blank"
rel="noreferrer noopener nofollow"
v-if="privacyPolicyUrl">
{{ $t('navigation.privacy') }}
</BaseButton>
<BaseButton
@click="$store.commit('keyboardShortcutsActive', true)"
class="dropdown-item"
>
</a>
<a @click="$store.commit('keyboardShortcutsActive', true)" class="dropdown-item">
{{ $t('keyboardShortcuts.title') }}
</BaseButton>
<BaseButton
:to="{name: 'about'}"
class="dropdown-item"
>
</a>
<router-link :to="{name: 'about'}" class="dropdown-item">
{{ $t('about.title') }}
</BaseButton>
<BaseButton
@click="logout()"
class="dropdown-item"
>
</router-link>
<a @click="logout()" class="dropdown-item">
{{ $t('user.auth.logout') }}
</BaseButton>
</a>
</dropdown>
</div>
</div>
</header>
</nav>
</template>
<script setup langs="ts">
import {ref, computed, onMounted, nextTick} from 'vue'
import {useStore} from 'vuex'
import {useRouter} from 'vue-router'
import {QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
<script>
import {mapState} from 'vuex'
import {CURRENT_LIST, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import Rights from '@/models/constants/rights.json'
import Update from '@/components/home/update.vue'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import Notifications from '@/components/notifications/notifications.vue'
import Logo from '@/components/home/Logo.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import MenuButton from '@/components/home/MenuButton.vue'
const store = useStore()
export default {
name: 'topNavigation',
components: {
Notifications,
Dropdown,
ListSettingsDropdown,
Update,
Logo,
MenuButton,
},
computed: {
...mapState({
userInfo: state => state.auth.info,
userAvatar: state => state.auth.avatarUrl,
userAuthenticated: state => state.auth.authenticated,
currentList: CURRENT_LIST,
background: 'background',
imprintUrl: state => state.config.legal.imprintUrl,
privacyPolicyUrl: state => state.config.legal.privacyPolicyUrl,
canWriteCurrentList: state => state.currentList.maxRight > Rights.READ,
}),
},
mounted() {
this.$nextTick(() => {
if (typeof this.$refs.usernameDropdown === 'undefined' || typeof this.$refs.listTitle === 'undefined') {
return
}
const userInfo = computed(() => store.state.auth.info)
const userAvatar = computed(() => store.state.auth.avatarUrl)
const currentList = computed(() => store.state.currentList)
const background = computed(() => store.state.background)
const imprintUrl = computed(() => store.state.config.legal.imprintUrl)
const privacyPolicyUrl = computed(() => store.state.config.legal.privacyPolicyUrl)
const canWriteCurrentList = computed(() => store.state.currentList.maxRight > Rights.READ)
const usernameDropdown = ref()
const listTitle = ref()
onMounted(async () => {
await nextTick()
if (typeof usernameDropdown.value === 'undefined' || typeof listTitle.value === 'undefined') {
return
}
const usernameWidth = usernameDropdown.value.$el.clientWidth
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
})
const router = useRouter()
function logout() {
store.dispatch('auth/logout')
router.push({name: 'user.login'})
}
function openQuickActions() {
store.commit(QUICK_ACTIONS_ACTIVE, true)
const usernameWidth = this.$refs.usernameDropdown.$el.clientWidth
this.$refs.listTitle.style.setProperty('--nav-username-width', `${usernameWidth}px`)
})
},
methods: {
logout() {
this.$store.dispatch('auth/logout')
this.$router.push({name: 'user.login'})
},
openQuickActions() {
this.$store.commit(QUICK_ACTIONS_ACTIVE, true)
},
},
}
</script>
@ -251,7 +247,6 @@ $hamburger-menu-icon-width: 28px;
border-radius: 100%;
vertical-align: middle;
height: 40px;
margin-right: var(--button-padding-horizontal);
}
:deep(.dropdown-trigger .button) {

View File

@ -85,8 +85,4 @@ export default {
margin-left: .5rem;
}
}
.dark .update-notification {
color: var(--grey-200);
}
</style>

View File

@ -66,7 +66,7 @@ const showIconOnly = computed(() => props.icon !== '' && typeof slots.default ==
text-transform: uppercase;
font-size: 0.85rem;
font-weight: bold;
min-height: $button-height;
height: $button-height;
box-shadow: var(--shadow-sm);
display: inline-flex;

View File

@ -1,85 +0,0 @@
<template>
<div class="password-field">
<input
class="input"
id="password"
name="password"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
:type="passwordFieldType"
autocomplete="current-password"
@keyup.enter="e => $emit('submit', e)"
:tabindex="props.tabindex"
@focusout="validate"
@input="handleInput"
/>
<a
@click="togglePasswordFieldType"
class="password-field-type-toggle"
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
</a>
</div>
<p class="help is-danger" v-if="!isValid">
{{ $t('user.auth.passwordRequired') }}
</p>
</template>
<script lang="ts" setup>
import {ref, watch} from 'vue'
import {useDebounceFn} from '@vueuse/core'
const props = defineProps({
tabindex: String,
modelValue: String,
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
validateInitially: Boolean,
})
const emit = defineEmits(['submit', 'update:modelValue'])
const passwordFieldType = ref<String>('password')
const password = ref<String>('')
const isValid = ref<Boolean>(!props.validateInitially)
watch(
() => props.validateInitially,
(doValidate: Boolean) => {
if (doValidate) {
validate()
}
},
)
function validate() {
useDebounceFn(() => {
isValid.value = password.value !== ''
}, 100)()
}
function togglePasswordFieldType() {
passwordFieldType.value = passwordFieldType.value === 'password'
? 'text'
: 'password'
}
function handleInput(e) {
password.value = e.target.value
emit('update:modelValue', e.target.value)
}
</script>
<style scoped>
.password-field {
position: relative;
}
.password-field-type-toggle {
position: absolute;
color: var(--grey-400);
top: 50%;
right: 1rem;
transform: translateY(-50%);
}
</style>

View File

@ -2,22 +2,21 @@
<dropdown>
<template v-if="isSavedFilter">
<dropdown-item
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'filter.settings.delete', params: { listId: list.id } }"
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
icon="trash-alt"
>
{{ $t('misc.delete') }}
</dropdown-item>
</template>
<template v-else-if="list.isArchived">
<dropdown-item
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
icon="archive"
>
{{ $t('menu.unarchive') }}
@ -25,38 +24,37 @@
</template>
<template v-else>
<dropdown-item
:to="{ name: 'list.settings.edit', params: { listId: list.id } }"
:to="{ name: `${listRoutePrefix}.edit`, params: { listId: list.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.background`, params: { listId: list.id } }"
v-if="backgroundsEnabled"
:to="{ name: 'list.settings.background', params: { listId: list.id } }"
icon="image"
>
{{ $t('menu.setBackground') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'list.settings.share', params: { listId: list.id } }"
:to="{ name: `${listRoutePrefix}.share`, params: { listId: list.id } }"
icon="share-alt"
>
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'list.settings.duplicate', params: { listId: list.id } }"
:to="{ name: `${listRoutePrefix}.duplicate`, params: { listId: list.id } }"
icon="paste"
>
{{ $t('menu.duplicate') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'list.settings.archive', params: { listId: list.id } }"
:to="{ name: `${listRoutePrefix}.archive`, params: { listId: list.id } }"
icon="archive"
>
{{ $t('menu.archive') }}
</dropdown-item>
<task-subscription
v-if="subscription"
class="dropdown-item has-no-shadow"
:is-button="false"
entity="list"
@ -65,7 +63,7 @@
@change="sub => subscription = sub"
/>
<dropdown-item
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
:to="{ name: `${listRoutePrefix}.delete`, params: { listId: list.id } }"
icon="trash-alt"
class="has-text-danger"
>
@ -75,32 +73,56 @@
</dropdown>
</template>
<script setup lang="ts">
import {ref, computed, watchEffect} from 'vue'
import {useStore} from 'vuex'
<script>
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
import ListModel from '@/models/list'
import SubscriptionModel from '@/models/subscription'
const props = defineProps({
list: {
type: ListModel,
required: true,
export default {
name: 'list-settings-dropdown',
data() {
return {
subscription: null,
}
},
})
components: {
TaskSubscription,
DropdownItem,
Dropdown,
},
props: {
list: {
required: true,
},
},
mounted() {
this.subscription = this.list.subscription
},
computed: {
backgroundsEnabled() {
return this.$store.state.config.enabledBackgroundProviders !== null && this.$store.state.config.enabledBackgroundProviders.length > 0
},
listRoutePrefix() {
let name = 'list'
const subscription = ref<SubscriptionModel>()
watchEffect(() => {
if (props.list.subscription) {
subscription.value = props.list.subscription
}
})
const store = useStore()
const backgroundsEnabled = computed(() => store.state.config.enabledBackgroundProviders?.length > 0)
const isSavedFilter = computed(() => getSavedFilterIdFromListId(props.list.id) > 0)
if (this.$route.name !== null && this.$route.name.startsWith('list.')) {
// HACK: we should implement a better routing for the modals
const settingsRoutes = ['edit', 'delete', 'archive', 'background', 'share', 'duplicate']
const suffix = settingsRoutes.find((route) => this.$route.name.endsWith(`.settings.${route}`))
name = this.$route.name.replace(`.settings.${suffix}`,'')
}
if (this.isSavedFilter) {
name = name.replace('list.', 'filter.')
}
return `${name}.settings`
},
isSavedFilter() {
return getSavedFilterIdFromListId(this.list.id) > 0
},
},
}
</script>

View File

@ -29,10 +29,9 @@
<script>
import Filters from '@/components/list/partials/filters'
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
import Popup from '@/components/misc/popup'
import {getDefaultParams} from '@/composables/taskList'
export default {
name: 'filter-popup',
components: {

View File

@ -191,7 +191,7 @@ import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/composables/taskList'
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
// FIXME: merge with DEFAULT_PARAMS in taskList.js
const DEFAULT_PARAMS = {

View File

@ -2,46 +2,53 @@
<router-link
:class="{
'has-light-text': !colorIsDark(list.hexColor),
'has-background': background !== null
'has-background': blurHashUrl !== ''
}"
:style="{
'background-color': list.hexColor,
'background-image': background !== null ? `url(${background})` : false,
'background-image': blurHashUrl !== null ? `url(${blurHashUrl})` : false,
}"
:to="{ name: 'list.index', params: { listId: list.id} }"
class="list-card"
v-if="list !== null && (showArchived ? true : !list.isArchived)"
>
<div class="is-archived-container">
<div
class="list-background background-fade-in"
:class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : false}"></div>
<div class="list-content">
<div class="is-archived-container">
<span class="is-archived" v-if="list.isArchived">
{{ $t('namespace.archived') }}
</span>
<span
:class="{'is-favorite': list.isFavorite, 'is-archived': list.isArchived}"
@click.stop="toggleFavoriteList(list)"
class="favorite">
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" />
<span
:class="{'is-favorite': list.isFavorite, 'is-archived': list.isArchived}"
@click.stop="toggleFavoriteList(list)"
class="favorite">
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/>
</span>
</div>
<div class="title">{{ list.title }}</div>
</div>
<div class="title">{{ list.title }}</div>
</router-link>
</template>
<script lang="ts" setup>
import {PropType, ref, watch} from 'vue'
import {ref, watch} from 'vue'
import {useStore} from 'vuex'
import ListService from '@/services/list'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import ListModel from '@/models/list'
const background = ref<string | null>(null)
const backgroundLoading = ref(false)
const blurHashUrl = ref('')
const props = defineProps({
list: {
type: Object as PropType<ListModel>,
type: Object,
required: true,
},
showArchived: {
@ -50,13 +57,18 @@ const props = defineProps({
},
})
watch(props.list, loadBackground, { immediate: true })
watch(props.list, loadBackground, {immediate: true})
async function loadBackground() {
if (props.list === null || !props.list.backgroundInformation || backgroundLoading.value) {
return
}
const blurHash = await getBlobFromBlurHash(props.list.backgroundBlurHash)
if (blurHash) {
blurHashUrl.value = window.URL.createObjectURL(blurHash)
}
backgroundLoading.value = true
const listService = new ListService()
@ -69,7 +81,7 @@ async function loadBackground() {
const store = useStore()
function toggleFavoriteList(list: ListModel) {
function toggleFavoriteList(list) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
@ -81,129 +93,145 @@ function toggleFavoriteList(list: ListModel) {
<style lang="scss" scoped>
.list-card {
cursor: pointer;
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
height: $list-height;
background: var(--white);
margin: 0 $list-spacing $list-spacing 0;
padding: 1rem;
border-radius: $radius;
box-shadow: var(--shadow-sm);
transition: box-shadow $transition;
cursor: pointer;
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
height: $list-height;
background: var(--white);
margin: 0 $list-spacing $list-spacing 0;
border-radius: $radius;
box-shadow: var(--shadow-sm);
transition: box-shadow $transition;
position: relative;
overflow: hidden;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
&.has-light-text .title {
color: var(--light);
}
&:hover {
box-shadow: var(--shadow-md);
}
&.has-background, .list-background {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
&:active,
&:focus,
&:focus:not(:active) {
box-shadow: var(--shadow-xs) !important;
}
&.has-background .list-content .title {
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
color: var(--white);
}
@media screen and (min-width: $widescreen) {
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
.list-background {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
@media screen and (max-width: $widescreen) and (min-width: $tablet) {
$lists-per-row: 3;
& {
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
}
&:hover {
box-shadow: var(--shadow-md);
}
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
&:active,
&:focus,
&:focus:not(:active) {
box-shadow: var(--shadow-xs) !important;
}
@media screen and (max-width: $tablet) {
$lists-per-row: 2;
& {
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
}
@media screen and (min-width: $widescreen) {
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
@media screen and (max-width: $widescreen) and (min-width: $tablet) {
$lists-per-row: 3;
& {
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
}
@media screen and (max-width: $mobile) {
$lists-per-row: 1;
& {
width: 100%;
margin-right: 0;
}
}
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
.is-archived-container {
width: 100%;
text-align: right;
@media screen and (max-width: $tablet) {
$lists-per-row: 2;
& {
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
}
.is-archived {
font-size: .75rem;
float: left;
}
}
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
.title {
align-self: flex-end;
font-family: $vikunja-font;
font-weight: 400;
font-size: 1.5rem;
color: var(--text);
width: 100%;
margin-bottom: 0;
max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge
overflow: hidden;
text-overflow: ellipsis;
@media screen and (max-width: $mobile) {
$lists-per-row: 1;
& {
width: 100%;
margin-right: 0;
}
}
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.list-content {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
padding: 1rem;
position: absolute;
height: 100%;
width: 100%;
&.has-light-text .title {
color: var(--light);
}
.is-archived-container {
width: 100%;
text-align: right;
&.has-background {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
.is-archived {
font-size: .75rem;
float: left;
}
}
.title {
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
color: var(--white);
}
}
.title {
align-self: flex-end;
font-family: $vikunja-font;
font-weight: 400;
font-size: 1.5rem;
color: var(--text);
width: 100%;
margin-bottom: 0;
max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge
overflow: hidden;
text-overflow: ellipsis;
.favorite {
transition: opacity $transition, color $transition;
opacity: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
&:hover {
color: var(--warning);
}
.favorite {
transition: opacity $transition, color $transition;
opacity: 0;
&.is-archived {
display: none;
}
&:hover {
color: var(--warning);
}
&.is-favorite {
display: inline-block;
opacity: 1;
color: var(--warning);
}
}
&.is-archived {
display: none;
}
&:hover .favorite {
opacity: 1;
}
&.is-favorite {
display: inline-block;
opacity: 1;
color: var(--warning);
}
}
&:hover .favorite {
opacity: 1;
}
}
}
</style>

View File

@ -39,66 +39,79 @@
</div>
</template>
<script setup lang="ts">
import {ref, computed, watch} from 'vue'
import { useI18n } from 'vue-i18n'
<script>
import Message from '@/components/misc/message'
import {parseURL} from 'ufo'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import {success} from '@/message'
import Message from '@/components/misc/message.vue'
const props = defineProps({
configureOpen: {
type: Boolean,
required: false,
default: false,
export default {
name: 'apiConfig',
components: {
Message,
},
})
const emit = defineEmits(['foundApi'])
const apiUrl = ref(window.API_URL)
const configureApi = ref(apiUrl.value === '')
const apiDomain = computed(() => parseURL(apiUrl.value).host || parseURL(window.location.href).host)
watch(() => props.configureOpen, (value) => {
configureApi.value = value
}, { immediate: true })
const {t} = useI18n()
const errorMsg = ref('')
const successMsg = ref('')
async function setApiUrl() {
if (apiUrl.value === '') {
// Don't try to check and set an empty url
errorMsg.value = t('apiConfig.urlRequired')
return
}
try {
const url = await checkAndSetApiUrl(apiUrl.value)
if (url === '') {
// If the config setter function could not figure out a url
throw new Error('URL cannot be empty.')
data() {
return {
configureApi: false,
apiUrl: window.API_URL,
errorMsg: '',
successMsg: '',
}
},
emits: ['foundApi'],
created() {
if (this.apiUrl === '') {
this.configureApi = true
}
},
computed: {
apiDomain() {
return parseURL(this.apiUrl).host || parseURL(window.location.href).host
},
},
props: {
configureOpen: {
type: Boolean,
required: false,
default: false,
},
},
watch: {
configureOpen: {
handler(value) {
this.configureApi = value
},
immediate: true,
},
},
methods: {
async setApiUrl() {
if (this.apiUrl === '') {
// Don't try to check and set an empty url
this.errorMsg = this.$t('apiConfig.urlRequired')
return
}
// Set it + save it to local storage to save us the hoops
errorMsg.value = ''
apiUrl.value = url
success({message: t('apiConfig.success', {domain: apiDomain.value})})
configureApi.value = false
emit('foundApi', apiUrl.value)
} catch (e) {
// Still not found, url is still invalid
successMsg.value = ''
errorMsg.value = t('apiConfig.error', {domain: apiDomain.value})
}
try {
const url = await checkAndSetApiUrl(this.apiUrl)
if (url === '') {
// If the config setter function could not figure out a url
throw new Error('URL cannot be empty.')
}
// Set it + save it to local storage to save us the hoops
this.errorMsg = ''
this.$message.success({message: this.$t('apiConfig.success', {domain: this.apiDomain})})
this.configureApi = false
this.apiUrl = url
this.$emit('foundApi', this.apiUrl)
} catch (e) {
// Still not found, url is still invalid
this.successMsg = ''
this.errorMsg = this.$t('apiConfig.error', {domain: this.apiDomain})
}
},
},
}
</script>

View File

@ -4,11 +4,9 @@
<template v-for="(s, i) in shortcuts" :key="i">
<h3>{{ $t(s.title) }}</h3>
<message class="mb-4" v-if="s.available">
<message>
{{
s.available($route)
? $t('keyboardShortcuts.currentPageOnly')
: $t('keyboardShortcuts.allPages')
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
}}
</message>
@ -19,8 +17,7 @@
class="shortcut-keys"
is="dd"
:keys="sc.keys"
:combination="sc.combination && $t(`keyboardShortcuts.${sc.combination}`)"
/>
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
</template>
</dl>
</template>
@ -28,18 +25,28 @@
</modal>
</template>
<script lang="ts" setup>
import {useStore} from 'vuex'
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message.vue'
<script>
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts'
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message'
import {KEYBOARD_SHORTCUTS} from './shortcuts'
const store = useStore()
function close() {
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
export default {
name: 'keyboard-shortcuts',
components: {
Message,
Shortcut,
},
data() {
return {
shortcuts: KEYBOARD_SHORTCUTS,
}
},
methods: {
close() {
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
},
},
}
</script>

View File

@ -1,24 +1,11 @@
import {RouteLocation} from 'vue-router'
import {isAppleDevice} from '@/helpers/isAppleDevice'
const ctrl = isAppleDevice() ? '⌘' : 'ctrl'
interface Shortcut {
title: string
keys: string[]
combination?: 'then'
}
interface ShortcutGroup {
title: string
available?: (route: RouteLocation) => boolean
shortcuts: Shortcut[]
}
export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
export const KEYBOARD_SHORTCUTS = [
{
title: 'keyboardShortcuts.general',
available: () => null,
shortcuts: [
{
title: 'keyboardShortcuts.toggleMenu',
@ -42,7 +29,7 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
},
{
title: 'keyboardShortcuts.list.title',
available: (route) => (route.name as string)?.startsWith('list.'),
available: (route) => route.name.startsWith('list.'),
shortcuts: [
{
title: 'keyboardShortcuts.list.switchToListView',
@ -68,7 +55,13 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
},
{
title: 'keyboardShortcuts.task.title',
available: (route) => route.name === 'task.detail',
available: (route) => [
'task.detail',
'task.list.detail',
'task.gantt.detail',
'task.kanban.detail',
'task.detail',
].includes(route.name),
shortcuts: [
{
title: 'keyboardShortcuts.task.assign',

View File

@ -1,35 +1,18 @@
<template>
<div class="message-wrapper">
<div class="message" :class="[variant, textAlignClass]">
<div class="message" :class="variant">
<slot/>
</div>
</div>
</template>
<script lang="ts" setup>
import {computed, PropType} from 'vue'
const TEXT_ALIGN_MAP = Object.freeze({
left: '',
center: 'has-text-centered',
right: 'has-text-right',
})
type textAlignVariants = keyof typeof TEXT_ALIGN_MAP
const props = defineProps({
defineProps({
variant: {
type: String,
default: 'info',
},
textAlign: {
type: String as PropType<textAlignVariants>,
default: 'left',
},
})
const textAlignClass = computed(() => TEXT_ALIGN_MAP[props.textAlign])
</script>
<style lang="scss" scoped>

View File

@ -14,9 +14,6 @@
<div>
<h2 class="title" v-if="title">{{ title }}</h2>
<api-config/>
<Message v-if="motd !== ''" class="is-hidden-tablet mb-4">
{{ motd }}
</Message>
<slot/>
</div>
<legal/>
@ -41,8 +38,8 @@ const store = useStore()
const {t} = useI18n()
const motd = computed(() => store.state.config.motd)
const title = computed(() => t(route.meta?.title as string || ''))
// @ts-ignore
const title = computed(() => t(route.meta.title ?? ''))
useTitle(() => title.value)
</script>

View File

@ -52,15 +52,9 @@ import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
import {useOnline} from '@/composables/useOnline'
import {useRouter, useRoute} from 'vue-router'
import {getAuthForRoute} from '@/router'
const router = useRouter()
const route = useRoute()
const store = useStore()
const ready = ref(false)
const ready = computed(() => store.state.vikunjaReady)
const online = useOnline()
const error = ref('')
@ -69,12 +63,7 @@ const showLoading = computed(() => !ready.value && error.value === '')
async function load() {
try {
await store.dispatch('loadApp')
const redirectTo = getAuthForRoute(route)
if (typeof redirectTo !== 'undefined') {
await router.push(redirectTo)
}
ready.value = true
} catch (e: any) {
} catch(e: any) {
error.value = e
}
}

View File

@ -1,51 +1,53 @@
<template>
<x-button
v-if="isButton"
variant="secondary"
:icon="iconName"
:icon="icon"
v-tooltip="tooltipText"
@click="changeSubscription"
:disabled="disabled || null"
v-if="isButton"
>
{{ buttonText }}
</x-button>
<BaseButton
v-else
<a
v-tooltip="tooltipText"
@click="changeSubscription"
:class="{'is-disabled': disabled}"
v-else
>
<span class="icon">
<icon :icon="iconName"/>
<icon :icon="icon"/>
</span>
{{ buttonText }}
</BaseButton>
</a>
</template>
<script lang="ts" setup>
import {computed, shallowRef} from 'vue'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue'
import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription'
import {success} from '@/message'
interface Props {
entity: string
entityId: number
subscription: SubscriptionModel
isButton?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isButton: true,
const props = defineProps({
entity: {
required: true,
type: String,
},
subscription: {
required: true,
},
entityId: {
required: true,
},
isButton: {
type: Boolean,
default: true,
},
})
const subscriptionEntity = computed<string>(() => props.subscription.entity)
const emit = defineEmits(['change'])
const subscriptionService = shallowRef(new SubscriptionService())
@ -55,7 +57,7 @@ const tooltipText = computed(() => {
if (disabled.value) {
return t('task.subscription.subscribedThroughParent', {
entity: props.entity,
parent: subscriptionEntity.value,
parent: props.subscription.entity,
})
}
@ -65,13 +67,13 @@ const tooltipText = computed(() => {
})
const buttonText = computed(() => props.subscription !== null ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const iconName = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const icon = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => {
if (props.subscription === null) {
return false
}
return subscriptionEntity.value !== props.entity
return props.subscription.entity !== props.entity
})
function changeSubscription() {

View File

@ -1,5 +1,4 @@
<template>
<!-- FIXME: transition should not be included in the modal -->
<transition name="modal">
<section
v-if="enabled"
@ -22,13 +21,6 @@
'is-wide': wide
}"
>
<BaseButton
@click="$emit('close')"
class="close"
>
<icon icon="times"/>
</BaseButton>
<slot>
<div class="header">
<slot name="header"></slot>
@ -61,8 +53,6 @@
</template>
<script>
import BaseButton from '@/components/base/BaseButton.vue'
export const TRANSITION_NAMES = {
MODAL: 'modal',
FADE: 'fade',
@ -80,11 +70,6 @@ function validValue(values) {
export default {
name: 'modal',
components: {
BaseButton,
},
mounted() {
document.addEventListener('keydown', (e) => {
// Close the model when escape is pressed
@ -212,22 +197,17 @@ export default {
}
}
.close {
position: fixed;
top: 5px;
right: 26px;
color: var(--white);
font-size: 2rem;
@media screen and (max-width: $desktop) {
color: var(--dark);
}
/* Transitions */
.modal-enter,
.modal-leave-active {
opacity: 0;
}
</style>
<style lang="scss">
// Close icon SVG uses currentColor, change the color to keep it visible
.dark .task-detail-view-modal .close {
color: var(--grey-900);
.modal-enter .modal-container,
.modal-leave-active .modal-container {
transform: scale(0.9);
}
</style>

View File

@ -13,7 +13,6 @@
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'])
@ -26,7 +25,7 @@ function findNamespaces(newQuery: string) {
query.value = newQuery
}
function select(namespace: NamespaceModel) {
function select(namespace) {
emit('selected', namespace)
}
</script>

View File

@ -16,13 +16,13 @@
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.share', params: { namespaceId: namespace.id } }"
:to="{ name: 'namespace.settings.share', params: { id: namespace.id } }"
icon="share-alt"
>
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'list.create', params: { namespaceId: namespace.id } }"
:to="{ name: 'list.create', params: { id: namespace.id } }"
icon="plus"
>
{{ $t('menu.newList') }}
@ -34,7 +34,6 @@
{{ $t('menu.archive') }}
</dropdown-item>
<task-subscription
v-if="subscription"
class="dropdown-item has-no-shadow"
:is-button="false"
entity="namespace"

View File

@ -264,6 +264,4 @@ export default {
.sharables-list:not(.card-content) {
overflow-y: auto
}
@include modal-transition();
</style>

View File

@ -365,7 +365,3 @@ export default {
},
}
</script>
<style lang="scss" scoped>
@include modal-transition();
</style>

View File

@ -67,7 +67,7 @@
<router-link
class="mt-2 has-text-centered is-block"
:to="taskDetailRoute"
:to="{name: 'task.detail', params: {id: taskEditTask.id}}"
>
{{ $t('task.openDetail') }}
</router-link>
@ -97,15 +97,6 @@ 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,

View File

@ -0,0 +1,101 @@
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)
}
},
},
}

View File

@ -34,7 +34,7 @@
>
<div class="filename">{{ a.file.name }}</div>
<div class="info">
<p class="attachment-info-meta">
<p class="collapses">
<i18n-t keypath="task.attachment.createdBy">
<span v-tooltip="formatDate(a.created)">
{{ formatDateSince(a.created) }}
@ -289,6 +289,21 @@ 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;
}
}
}
}
}
}
@ -326,10 +341,6 @@ export default {
height: auto;
text-shadow: var(--shadow-md);
animation: bounce 2s infinite;
@media (prefers-reduced-motion: reduce) {
animation: none;
}
}
.hint {
@ -346,35 +357,6 @@ 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%,
@ -400,6 +382,4 @@ export default {
transform: translate3d(0, -4px, 0);
}
}
@include modal-transition();
</style>

View File

@ -162,7 +162,7 @@ import {mapState} from 'vuex'
export default {
name: 'comments',
components: {
Editor: AsyncEditor,
editor: AsyncEditor,
},
props: {
taskId: {
@ -339,6 +339,4 @@ export default {
.media-content {
width: calc(100% - 48px - 2rem);
}
@include modal-transition();
</style>

View File

@ -1,47 +0,0 @@
<template>
<p class="created">
<time :datetime="formatISO(task.created)" v-tooltip="formatDate(task.created)">
<i18n-t keypath="task.detail.created">
<span>{{ formatDateSince(task.created) }}</span>
{{ task.createdBy.getDisplayName() }}
</i18n-t>
</time>
<template v-if="+new Date(task.created) !== +new Date(task.updated)">
<br/>
<!-- Computed properties to show the actual date every time it gets updated -->
<time :datetime="formatISO(task.updated)" v-tooltip="updatedFormatted">
<i18n-t keypath="task.detail.updated">
<span>{{ updatedSince }}</span>
</i18n-t>
</time>
</template>
<template v-if="task.done">
<br/>
<time :datetime="formatISO(task.doneAt)" v-tooltip="doneFormatted">
<i18n-t keypath="task.detail.doneAt">
<span>{{ doneSince }}</span>
</i18n-t>
</time>
</template>
</p>
</template>
<script lang="ts" setup>
import {computed, toRefs} from 'vue'
import TaskModel from '@/models/task'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
const props = defineProps({
task: {
type: TaskModel,
required: true,
},
})
const {task} = toRefs(props)
const updatedSince = computed(() => formatDateSince(task.value.updated))
const updatedFormatted = computed(() => formatDateLong(task.value.updated))
const doneSince = computed(() => formatDateSince(task.value.doneAt))
const doneFormatted = computed(() => formatDateLong(task.value.doneAt))
</script>

View File

@ -1,8 +1,6 @@
<template>
<td v-tooltip="+date === 0 ? '' : formatDate(date)">
<time :datetime="date ? formatISO(date) : null">
{{ +date === 0 ? '-' : formatDateSince(date) }}
</time>
{{ +date === 0 ? '-' : formatDateSince(date) }}
</td>
</template>

View File

@ -38,7 +38,7 @@ import {mapState} from 'vuex'
export default {
name: 'description',
components: {
Editor: AsyncEditor,
editor: AsyncEditor,
},
data() {
return {

View File

@ -19,19 +19,19 @@
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
class="tag">
<span>{{ props.item.title }}</span>
<button type="button" v-cy="'taskDetail.removeLabel'" @click="removeLabel(props.item)" class="delete is-small" />
<a @click="removeLabel(props.item)" class="delete is-small"></a>
</span>
</template>
<template #searchResult="props">
<span
v-if="typeof props.option === 'string'"
class="tag search-result">
class="tag">
<span>{{ props.option }}</span>
</span>
<span
v-else
:style="{'background': props.option.hexColor, 'color': props.option.textColor}"
class="tag search-result">
class="tag">
<span>{{ props.option.title }}</span>
</span>
</template>
@ -114,17 +114,23 @@ export default {
},
async removeLabel(label) {
if (!this.taskId === 0) {
await this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
const removeFromState = () => {
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)
}
for (const l in this.labels) {
if (this.labels[l].id === label.id) {
this.labels.splice(l, 1)
}
if (this.taskId === 0) {
removeFromState()
return
}
this.$emit('update:modelValue', this.labels)
this.$emit('change', this.labels)
await this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
removeFromState()
this.$message.success({message: this.$t('task.label.removeSuccess')})
},
@ -146,18 +152,6 @@ export default {
<style lang="scss" scoped>
.tag {
margin: .25rem !important;
}
.tag.search-result {
margin: 0 !important;
}
:deep(.input-wrapper) {
padding: .25rem !important;
}
:deep(input.input) {
padding: 0 .5rem;
margin: .5rem 0 0 .5rem;
}
</style>

View File

@ -7,8 +7,8 @@
'has-light-text': !colorIsDark(task.hexColor) && task.hexColor !== `#${task.defaultColor}` && task.hexColor !== task.defaultColor,
}"
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
@click.exact="openTaskDetail()"
@click.ctrl="() => toggleTaskDone(task)"
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
@click.meta="() => toggleTaskDone(task)"
>
<span class="task-id">
@ -28,9 +28,9 @@
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
<time :datetime="formatISO(task.dueDate)">
<span>
{{ formatDateSince(task.dueDate) }}
</time>
</span>
</span>
<h3>{{ task.title }}</h3>
<progress
@ -115,13 +115,6 @@ export default {
this.loadingInternal = false
}
},
openTaskDetail() {
this.$router.push({
name: 'task.detail',
params: { id: this.task.id },
state: { backdropView: this.$router.currentRoute.value.fullPath },
})
},
},
}
</script>
@ -138,6 +131,7 @@ $task-background: var(--white);
border: 3px solid transparent;
font-size: .9rem;
margin: .5rem;
padding: .4rem;
border-radius: $radius;
background: $task-background;

View File

@ -1,7 +1,7 @@
<template>
<div class="task-relations">
<x-button
v-if="editEnabled && Object.keys(relatedTasks).length > 0"
v-if="Object.keys(relatedTasks).length > 0"
@click="showNewRelationForm = !showNewRelationForm"
class="is-pulled-right add-task-relation-button"
:class="{'is-active': showNewRelationForm}"
@ -274,11 +274,10 @@ 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,
} = listAndNamespace === null ? {list: null, namespace: null} : listAndNamespace
} = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
return {
...task,
@ -365,6 +364,4 @@ export default {
:deep(.multiselect .search-results button) {
padding: 0.5rem;
}
@include modal-transition();
</style>

View File

@ -8,7 +8,7 @@
>
</span>
<router-link
:to="taskDetailRoute"
:to="{ name: taskDetailRoute, params: { id: task.id } }"
:class="{ 'done': task.done}"
class="tasktext">
<span>
@ -39,17 +39,14 @@
:user="a"
v-for="(a, i) in task.assignees"
/>
<time
:datetime="formatISO(task.dueDate)"
<i
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
@click.prevent.stop="showDefer = !showDefer"
v-if="+new Date(task.dueDate) > 0"
v-tooltip="formatDate(task.dueDate)"
:aria-expanded="showDefer ? 'true' : 'false'"
>
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</time>
</i>
<transition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</transition>
@ -129,6 +126,10 @@ export default {
type: Boolean,
default: false,
},
taskDetailRoute: {
type: String,
default: 'task.list.detail',
},
showList: {
type: Boolean,
default: false,
@ -166,14 +167,6 @@ 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) {

View File

@ -1,21 +1,20 @@
<template>
<BaseButton>
<a @click="$emit('click')">
<icon icon="sort-up" v-if="order === 'asc'"/>
<icon icon="sort-up" v-else-if="order === 'desc'" rotation="180"/>
<icon icon="sort-up" rotation="180" v-else-if="order === 'desc'"/>
<icon icon="sort" v-else/>
</BaseButton>
</a>
</template>
<script setup lang="ts">
import {PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
type Order = 'asc' | 'desc' | 'none'
defineProps({
order: {
type: String as PropType<Order>,
default: 'none',
<script>
export default {
name: 'sort',
props: {
order: {
type: String,
default: 'none',
},
},
})
emits: ['click'],
}
</script>

View File

@ -1,111 +0,0 @@
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,
}
}

View File

@ -1,9 +1,9 @@
import {computed, watch, readonly} from 'vue'
import {useStorage, createSharedComposable, BasicColorSchema, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
import {useStorage, createSharedComposable, ColorSchema, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
const STORAGE_KEY = 'color-scheme'
const DEFAULT_COLOR_SCHEME_SETTING: BasicColorSchema = 'light'
const DEFAULT_COLOR_SCHEME_SETTING: ColorSchema = '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<BasicColorSchema>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
const store = useStorage<ColorSchema>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING)
const preferredColorScheme = usePreferredColorScheme()

View File

@ -1,9 +1,9 @@
import { computed, watchEffect } from 'vue'
import { setTitle } from '@/helpers/setTitle'
import { ComputedGetter } from '@vue/reactivity'
import { ComputedGetter, ComputedRef } from '@vue/reactivity'
export function useTitle(titleGetter: ComputedGetter<string>) {
export function useTitle<T>(titleGetter: ComputedGetter<T>) : ComputedRef<T> {
const titleRef = computed(titleGetter)
watchEffect(() => setTitle(titleRef.value))

View File

@ -53,7 +53,6 @@ export async function refreshToken(persist: boolean): Promise<AxiosResponse> {
return response
} catch(e) {
// @ts-ignore
throw new Error('Error renewing token: ', { cause: e })
}
}

View File

@ -0,0 +1,31 @@
import {decode} from 'blurhash'
export async function getBlobFromBlurHash(blurHash: string): Promise<Blob | null> {
if (blurHash === '') {
return null
}
const pixels = decode(blurHash, 32, 32)
const canvas = document.createElement('canvas')
canvas.width = 32
canvas.height = 32
const ctx = canvas.getContext('2d')
if (ctx === null) {
return null
}
const imageData = ctx.createImageData(32, 32)
imageData.data.set(pixels)
ctx.putImageData(imageData, 0, 0)
return new Promise<Blob>((resolve, reject) => {
canvas.toBlob(b => {
if (b === null) {
reject(b)
return
}
resolve(b)
})
})
}

View File

@ -1,6 +0,0 @@
export function isEmail(email: string): Boolean {
const format = /^.+@.+$/
const match = email.match(format)
return match === null ? false : match.length > 0
}

View File

@ -1,5 +1,3 @@
// 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

View File

@ -1,4 +1,4 @@
export function setTitle(title : undefined | string) {
export function setTitle(title) {
document.title = (typeof title === 'undefined' || title === '')
? 'Vikunja'
: `${title} | Vikunja`

View File

@ -1,5 +1,5 @@
import {createDateFromString} from '@/helpers/time/createDateFromString'
import {format, formatDistanceToNow, formatISO as formatISOfns} from 'date-fns'
import {format, formatDistanceToNow} from 'date-fns'
import {enGB, de, fr, ru} from 'date-fns/locale'
import {i18n} from '@/i18n'
@ -44,7 +44,3 @@ export const formatDateSince = (date) => {
addSuffix: true,
})
}
export function formatISO(date) {
return date ? formatISOfns(date) : ''
}

View File

@ -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,18 +302,18 @@ const getDayFromText = (text: string) => {
const day = parseInt(results[0])
date.setDate(day)
// 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.
// 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.
// 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.
while (date < now) {
date.setMonth(date.getMonth() + 1)
}
if (date.getDate() !== day) {
if (day === 31 && date.getDate() !== day) {
date.setDate(day)
}
if (date < now) {
date.setMonth(date.getMonth() + 1)
}
return {
foundText: results[0],
date: date,

View File

@ -1,18 +1,7 @@
import axios from 'axios'
import {getToken} from '@/helpers/auth'
export function HTTPFactory() {
export const HTTPFactory = () => {
return axios.create({
baseURL: window.API_URL,
})
}
export function AuthenticatedHTTPFactory(token = getToken()) {
return axios.create({
baseURL: window.API_URL,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
})
}

View File

@ -1,4 +1,4 @@
import {createI18n} from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import langEN from './lang/en.json'
export const i18n = createI18n({
@ -19,9 +19,6 @@ export const availableLanguages = {
'vi-VN': 'Tiếng Việt',
'it-IT': 'Italiano',
'cs-CZ': 'Čeština',
'pl-PL': 'Polski',
'nl-NL': 'Nederlands',
'pt-PT': 'Português',
}
const loadedLanguages = ['en'] // our default language that is preloaded
@ -33,10 +30,6 @@ const setI18nLanguage = lang => {
}
export const loadLanguageAsync = lang => {
if (!lang) {
return
}
if (
// If the same language
i18n.global.locale === lang ||

View File

@ -31,9 +31,10 @@
"username": "Uživatelské jméno",
"usernameEmail": "Uživatelské jméno nebo e-mail",
"usernamePlaceholder": "např. Jarmil",
"email": "Email address",
"email": "E-mailová adresa",
"emailPlaceholder": "např. jarmil{'@'}vikunja.io",
"password": "Heslo",
"passwordRepeat": "Zopakovat heslo",
"passwordPlaceholder": "např. • • • • • • • •",
"forgotPassword": "Zapomenuté heslo?",
"resetPassword": "Obnovit heslo",
@ -44,20 +45,12 @@
"totpTitle": "Kód dvoufaktorového ověření",
"totpPlaceholder": "např. 123456",
"login": "Přihlásit se",
"createAccount": "Create account",
"register": "Registrovat",
"loginWith": "Přihlásit se pomocí {provider}",
"authenticating": "Ověřování…",
"openIdStateError": "Stav neodpovídá, odmítám pokračovat!",
"openIdGeneralError": "Došlo k chybě při ověřování proti třetí straně.",
"logout": "Odhlásit se",
"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?",
"remember": "Stay logged in"
"logout": "Odhlásit se"
},
"settings": {
"title": "Nastavení",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Vaše současné heslo",
"passwordsDontMatch": "Nové heslo se neshoduje s potvrzením hesla.",
"passwordUpdateSuccess": "Heslo bylo úspěšně změněno.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Aktualizovat Vaši e-mailovou adresu",
"updateEmailNew": "Nová e-mailová adresa",
"updateEmailSuccess": "Vaše e-mailová adresa byla úspěšně aktualizována. Poslali jsme vám odkaz pro její potvrzení.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Neděle",
"weekStartMonday": "Pondělí",
"language": "Jazyk",
"defaultList": "Výchozí seznam",
"timezone": "Time Zone"
"defaultList": "Výchozí seznam"
},
"totp": {
"title": "Dvoufaktorové ověření",
@ -335,7 +327,6 @@
"archiveText": "Nebudete moci upravovat tento jmenný prostor ani vytvářet nové seznamy, dokud jej neodarchivujete. Všechny seznamy v tomto prostoru budou také archivovány.",
"unarchiveText": "Budete moci vytvářet nové úkoly nebo je upravovat.",
"success": "Prostor byl úspěšně archivován.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "Pokud je prostor archivován, nelze vytvořit nové seznamy nebo je upravit."
},
"delete": {

View File

@ -7,7 +7,7 @@
"lastViewed": "Zuletzt angesehen",
"list": {
"newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:",
"new": "Neue Liste",
"new": "New list",
"importText": "Oder importiere deine Listen und Aufgaben aus anderen Diensten in Vikunja:",
"import": "Deine Daten in Vikunja importieren"
}
@ -34,6 +34,7 @@
"email": "E-Mail-Adresse",
"emailPlaceholder": "z.B. frederic{'@'}vikunja.io",
"password": "Passwort",
"passwordRepeat": "Gib dein Passwort erneut ein",
"passwordPlaceholder": "z.B. •••••••••••",
"forgotPassword": "Passwort vergessen?",
"resetPassword": "Setze dein Passwort zurück",
@ -44,20 +45,12 @@
"totpTitle": "Zwei-Faktor-Authentifizierungscode",
"totpPlaceholder": "z.B. 123456",
"login": "Anmelden",
"createAccount": "Account erstellen",
"register": "Registrieren",
"loginWith": "Mit {provider} anmelden",
"authenticating": "Authentifizierung…",
"openIdStateError": "Zustand stimmt nicht überein, fahre nicht fort!",
"openIdGeneralError": "Es ist ein Fehler bei der externen Authentisierung aufgetreten.",
"logout": "Abmelden",
"emailInvalid": "Bitte gib eine gültige E-Mail-Adresse ein.",
"usernameRequired": "Bitte gib einen Anmeldenamen ein.",
"passwordRequired": "Bitte gib ein Passwort ein.",
"showPassword": "Passwort anzeigen",
"hidePassword": "Passwort verbergen",
"noAccountYet": "Noch kein Account?",
"alreadyHaveAnAccount": "Hast du bereits einen Account?",
"remember": "Stay logged in"
"logout": "Abmelden"
},
"settings": {
"title": "Einstellungen",
@ -85,8 +78,7 @@
"weekStartSunday": "Sonntag",
"weekStartMonday": "Montag",
"language": "Sprache",
"defaultList": "Standard-Liste",
"timezone": "Time Zone"
"defaultList": "Standard-Liste"
},
"totp": {
"title": "Zwei-Faktor-Authentifizierung",
@ -165,7 +157,7 @@
"searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen",
"shared": "Geteilte Listen",
"create": {
"header": "Neue Liste",
"header": "New list",
"titlePlaceholder": "Der Titel der Liste steht hier…",
"addTitleRequired": "Bitte gebe einen Namen an.",
"createdSuccess": "Die Liste wurde erfolgreich erstellt.",
@ -323,7 +315,7 @@
"namespaces": "Namespaces",
"search": "Beginne zu schreiben, um einen Namespace zu suchen…",
"create": {
"title": "Neuer Namespace",
"title": "New 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?",
@ -335,7 +327,6 @@
"archiveText": "Du kannst diesen Namespace nicht mehr bearbeiten oder neue Listen erstellen, bis du die Archivierung rückgängig machst. Das gilt auch für alle Listen in diesem Namespace.",
"unarchiveText": "Du kannst neue Aufgaben erstellen oder diese bearbeiten.",
"success": "Der Namespace wurde erfolgreich archiviert.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "In einem archivierten Namespace können Listen weder angelegt noch editiert werden."
},
"delete": {
@ -392,7 +383,7 @@
"reminderRange": "Erinnerungs-Datumsbereich"
},
"create": {
"title": "Neuer gespeicherter Filter",
"title": "New Saved 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"
},
@ -554,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": "Als erledigt markieren!",
"done": "Mark task done!",
"undone": "Als nicht erledigt markieren",
"created": "Erstellt {0} von {1}",
"updated": "Aktualisiert {0}",
@ -790,7 +781,7 @@
"then": "dann",
"task": {
"title": "Aufgabenseite",
"done": "Fertig",
"done": "Done",
"assign": "Benutzer:in zuweisen",
"labels": "Dieser Aufgabe ein Label hinzufügen",
"dueDate": "Ändere das Fälligkeitsdatum dieser Aufgabe",
@ -908,7 +899,7 @@
"4015": "Dieser Aufgabenkommentar existiert nicht.",
"4016": "Ungültiges Aufgabenfeld.",
"4017": "Ungültiger Aufgabenfilter (Vergleichskriterium).",
"4018": "Ungültige Verkettung von Aufgabenfiltern.",
"4018": "Invalid task filter concatenator.",
"4019": "Ungültiger Aufgabenfilter (Wert).",
"5001": "Dieser Namespace existiert nicht.",
"5003": "Du hast keinen Zugriff auf den Namespace.",

View File

@ -7,7 +7,7 @@
"lastViewed": "Zletscht ahglueget",
"list": {
"newText": "Du chasch e Liste für dini neue Uufgabe erstelle:",
"new": "Neue Liste",
"new": "New list",
"importText": "Oder importier dini Liste und Uufgabe us anderne Dienst nach Vikunja:",
"import": "Dini Date in Vikunja importiere"
}
@ -31,9 +31,10 @@
"username": "Benutzernamä",
"usernameEmail": "Benutzernamä oder E-Mail Adrässe",
"usernamePlaceholder": "z.B. Hansruedi",
"email": "E-Mail-Adresse",
"email": "E-Mail Adrässe",
"emailPlaceholder": "z.B. frederic{'@'}vikunja.io",
"password": "Passwort",
"passwordRepeat": "Gib dis Passwort nomal iih",
"passwordPlaceholder": "z.B. •••••••••••",
"forgotPassword": "Passwort vergessen?",
"resetPassword": "Setz diis Passwort zrugg",
@ -44,20 +45,12 @@
"totpTitle": "Zweifaktor Authentifizierigs Ziffere",
"totpPlaceholder": "z.B. 123456",
"login": "Iihlogge",
"createAccount": "Account erstellen",
"register": "Registriere",
"loginWith": "Iihlogge mit {provider}",
"authenticating": "Authentifiziere…",
"openIdStateError": "Status stimmt nid überiih, ich verweigerä wiiter zmache!",
"openIdGeneralError": "Es ist ein Fehler bei der externen Authentisierung aufgetreten.",
"logout": "Uuslogge",
"emailInvalid": "Bitte gib eine gültige E-Mail-Adresse ein.",
"usernameRequired": "Bitte gib einen Anmeldenamen ein.",
"passwordRequired": "Bitte gib ein Passwort ein.",
"showPassword": "Passwort anzeigen",
"hidePassword": "Passwort verbergen",
"noAccountYet": "Noch kein Account?",
"alreadyHaveAnAccount": "Hast du bereits einen Account?",
"remember": "Stay logged in"
"logout": "Uuslogge"
},
"settings": {
"title": "Iihstellige",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Diis jetzige Passwort",
"passwordsDontMatch": "Dis neue Passwort und siini Bestätigung stimmed nid überiih.",
"passwordUpdateSuccess": "Dis Passwort isch erfolgriich aktualisiert wordä.",
"updateEmailTitle": "Aktualisiere deine E-Mail-Adresse",
"updateEmailTitle": "Dini E-Mail Adrässä änderä",
"updateEmailNew": "Neui E-Mail Adrässä",
"updateEmailSuccess": "Dini E-Mail Adrässä isch erfolgriich gänderet worde. Mir hend dir en Link gschickt, um si zu bestätigä.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Sunntig",
"weekStartMonday": "Määntig",
"language": "Sproch",
"defaultList": "Standard Liste",
"timezone": "Time Zone"
"defaultList": "Standard Liste"
},
"totp": {
"title": "Zweifaktor Authentifizierig",
@ -103,9 +95,9 @@
"disableSuccess": "Zweifaktor Authentifizierig isch erfolgriich uusgschalte wore."
},
"caldav": {
"title": "CalDAV",
"howTo": "Du chasch Vikunja zu CalDAV Applikatione verbinde, um dini Uufgabe vo verschidene Gräät zgseh. Gib die Url i dim Client iih:",
"more": "Meh Informatione über CalDAV in Vikunja"
"title": "Caldav",
"howTo": "Du chasch Vikunja zu Caldav Applikatione verbinde, um dini Uufgabe vo verschidene Gräät zgseh. Gib die Url i dim Client iih:",
"more": "Meh Informatione über Caldav in Vikunja"
},
"avatar": {
"title": "Herr Der Elemente",
@ -165,7 +157,7 @@
"searchSelect": "Druck uf Enter um die Liste uuszwähle",
"shared": "Teilti Liste",
"create": {
"header": "Neue Liste",
"header": "New list",
"titlePlaceholder": "Listetitl da ahgeh…",
"addTitleRequired": "Bitte gib en Titl ah.",
"createdSuccess": "Liste erfolgriich erstellt.",
@ -323,7 +315,7 @@
"namespaces": "Namensrüüm",
"search": "Schriib, um nachemne Namensruum z'sueche…",
"create": {
"title": "Neuer Namespace",
"title": "New 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?",
@ -335,7 +327,6 @@
"archiveText": "Du hesch kei möglichkeit meh de Namensruum z'bearbeite oder neui Listene drin z'erstelle, bis du si wider ent-archiviert hesch. Das archiviert au grad alli Liste im Namensruum.",
"unarchiveText": "Du chasch neui Liste erstelle oder bearbeite.",
"success": "De Namensruum isch erfolgriich archiviert worde.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "Wenn en Namensruum archiviert isch, chasch du kei neui Liste erstelle oder die bearbeite."
},
"delete": {
@ -392,7 +383,7 @@
"reminderRange": "Errinnerigs Datumbereich"
},
"create": {
"title": "Neuer gespeicherter Filter",
"title": "New Saved 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"
},
@ -554,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": "Als erledigt markieren!",
"done": "Mark task done!",
"undone": "Als unerledigt markierä",
"created": "Erstellt am {0} vo {1}",
"updated": "{0} g'updatet",
@ -790,7 +781,7 @@
"then": "dann",
"task": {
"title": "Uufgabesiite",
"done": "Fertig",
"done": "Done",
"assign": "Benutzer:in zuweisen",
"labels": "Labels ennere Uufgab hinzuefüege",
"dueDate": "S'Fälligkeitsdatum für die Uufgab ändere",
@ -908,7 +899,7 @@
"4015": "De Uufgabe Kommentar giz nid.",
"4016": "Ungültigs Uufgabefeld.",
"4017": "Ungültige Uufgabefilter vergliich.",
"4018": "Ungültige Verkettung von Aufgabenfiltern.",
"4018": "Invalid task filter concatenator.",
"4019": "Ungültigi Uufgabe Filter Wert.",
"5001": "De Namensruum existiert nid.",
"5003": "Du hesch kei Zuegriff zu dem Namensruum.",

View File

@ -31,9 +31,10 @@
"username": "Username",
"usernameEmail": "Username Or Email Address",
"usernamePlaceholder": "e.g. frederick",
"email": "Email address",
"email": "E-mail address",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password",
@ -44,20 +45,12 @@
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"createAccount": "Create account",
"register": "Register",
"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",
"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?",
"remember": "Stay logged in"
"logout": "Logout"
},
"settings": {
"title": "Settings",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Update Your E-Mail Address",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Sunday",
"weekStartMonday": "Monday",
"language": "Language",
"defaultList": "Default List",
"timezone": "Time Zone"
"defaultList": "Default List"
},
"totp": {
"title": "Two Factor Authentication",
@ -335,7 +327,6 @@
"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.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new lists or edit it."
},
"delete": {

View File

@ -31,9 +31,10 @@
"username": "Username",
"usernameEmail": "Username Or Email Address",
"usernamePlaceholder": "e.g. frederick",
"email": "Email address",
"email": "E-mail address",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password",
@ -44,20 +45,12 @@
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"createAccount": "Create account",
"register": "Register",
"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",
"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?",
"remember": "Stay logged in"
"logout": "Logout"
},
"settings": {
"title": "Settings",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Update Your E-Mail Address",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Sunday",
"weekStartMonday": "Monday",
"language": "Language",
"defaultList": "Default List",
"timezone": "Time Zone"
"defaultList": "Default List"
},
"totp": {
"title": "Two Factor Authentication",
@ -335,7 +327,6 @@
"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.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new lists or edit it."
},
"delete": {

View File

@ -31,9 +31,10 @@
"username": "Nom dutilisateur·rice",
"usernameEmail": "Nom dutilisateur·rice ou adresse courriel",
"usernamePlaceholder": "p. ex. frederick",
"email": "Email address",
"email": "Adresse courriel",
"emailPlaceholder": "p. ex. frederic{'@'}vikunja.io",
"password": "Mot de passe",
"passwordRepeat": "Retape ton mot de passe",
"passwordPlaceholder": "p. ex. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Réinitialiser ton mot de passe",
@ -44,20 +45,12 @@
"totpTitle": "Code dauthentification à deux facteurs",
"totpPlaceholder": "p. ex. 123456",
"login": "Se connecter",
"createAccount": "Create account",
"register": "Sinscrire",
"loginWith": "Se connecter avec {provider}",
"authenticating": "Authentification…",
"openIdStateError": "Létat ne correspond pas, impossible de continuer !",
"openIdGeneralError": "Une erreur s'est produite lors de l'authentification contre un tiers.",
"logout": "Se déconnecter",
"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?",
"remember": "Stay logged in"
"logout": "Se déconnecter"
},
"settings": {
"title": "Paramètres",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Ton mot de passe actuel",
"passwordsDontMatch": "Le nouveau mot de passe et sa confirmation ne correspondent pas.",
"passwordUpdateSuccess": "Mot de passe mis à jour.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Mets à jour ton adresse électronique",
"updateEmailNew": "Nouvelle adresse courriel",
"updateEmailSuccess": "Mise à jour de ladresse électronique. Clique sur le lien dans le courriel qui ta été envoyé pour le confirmer.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "dimanche",
"weekStartMonday": "lundi",
"language": "Langue",
"defaultList": "Liste par défaut",
"timezone": "Time Zone"
"defaultList": "Liste par défaut"
},
"totp": {
"title": "Authentification à deux facteurs",
@ -124,12 +116,12 @@
"vikunja": "Vikunja"
},
"appearance": {
"title": "Jeu de couleurs",
"setSuccess": "Changement du jeu de couleurs enregistré vers {colorScheme}",
"title": "Color Scheme",
"setSuccess": "Saved change of color scheme to {colorScheme}",
"colorScheme": {
"light": "Clair",
"system": "Système",
"dark": "Sombre"
"light": "Light",
"system": "System",
"dark": "Dark"
}
}
},
@ -335,7 +327,6 @@
"archiveText": "Tu ne pourras pas modifier cet espace de noms ou créer de nouvelles listes tant que tu ne lauras pas désarchivé. Ceci archivera également toutes les listes de cet espace de noms.",
"unarchiveText": "Tu pourras créer de nouvelles listes ou les modifier.",
"success": "Espace de noms archivé.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "Larchivage dun espace de noms signifie quon ne peut pas créer de nouvelles listes dans cet espace, ni le modifier."
},
"delete": {
@ -484,7 +475,7 @@
"download": "Télécharger",
"showMenu": "Afficher le menu",
"hideMenu": "Masquer le menu",
"forExample": "Par exemple :",
"forExample": "For example:",
"welcomeBack": "Welcome Back!"
},
"input": {
@ -570,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": "Attribuer à un utilisateur",
"assign": "Assign to a user",
"label": "Ajouter des étiquettes",
"priority": "Définir la priorité",
"dueDate": "Définir léchéance",
@ -735,8 +726,8 @@
"dateCurrentYear": "utilisera lannée en cours",
"dateNth": "utilisera le {day}e du mois en cours",
"dateTime": "Combinez nimporte lequel des formats de date avec « {time} » (ou {timePM}) pour définir une heure.",
"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)."
"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)."
}
},
"team": {
@ -791,7 +782,7 @@
"task": {
"title": "Page de tâche",
"done": "Done",
"assign": "Attribuer à un utilisateur",
"assign": "Assign to a user",
"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",
@ -917,7 +908,7 @@
"5010": "Cette équipe na pas accès à cet espace de noms.",
"5011": "Cet·e utilisateur·rice a déjà accès à cet espace de noms.",
"5012": "Lespace de noms est archivé et ne peut donc être consulté quen lecture seule.",
"6001": "Le nom de l'équipe ne peut pas être vide.",
"6001": "The team name cannot be empty.",
"6002": "Léquipe nexiste pas.",
"6004": "Léquipe a déjà accès à cet espace de noms ou à cette liste.",
"6005": "Lutilisateur·rice est déjà membre de cette équipe.",

View File

@ -7,7 +7,7 @@
"lastViewed": "Ultima visualizzazione",
"list": {
"newText": "È possibile creare una nuova lista per le nuove attività:",
"new": "Nuova lista",
"new": "New list",
"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 sta caricando…",
"errorOccured": "Si è verificato un errore:",
"checkApiUrl": "Controlla se l'URL API è corretto.",
"noApiUrlConfigured": "Nessun URL API configurato. Impostane uno qui sotto:"
"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:"
},
"offline": {
"title": "Sei offline.",
"text": "Controlla la connessione di rete e riprova."
"title": "You are offline.",
"text": "Please check your network connection and try again."
},
"user": {
"auth": {
@ -34,8 +34,9 @@
"email": "Indirizzo e-mail",
"emailPlaceholder": "per es. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Digita di nuovo la tua password",
"passwordPlaceholder": "es. ••••••••••••",
"forgotPassword": "Password dimenticata?",
"forgotPassword": "Forgot your password?",
"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.",
@ -44,20 +45,12 @@
"totpTitle": "Codice di autenticazione a due fattori",
"totpPlaceholder": "es. 123456",
"login": "Accedi",
"createAccount": "Crea account",
"register": "Registrati",
"loginWith": "Accedi con {provider}",
"authenticating": "Autenticazione…",
"openIdStateError": "Stato non corrispondente, impossibile continuare!",
"openIdStateError": "State does not match, refusing to continue!",
"openIdGeneralError": "Si è verificato un errore durante l'autenticazione con terze parti.",
"logout": "Esci",
"emailInvalid": "Inserisci un indirizzo e-mail valido.",
"usernameRequired": "Inserisci un nome utente.",
"passwordRequired": "Inserisci una password.",
"showPassword": "Mostra la password",
"hidePassword": "Nascondi la password",
"noAccountYet": "Non hai un account?",
"alreadyHaveAnAccount": "Hai già un account?",
"remember": "Resta connesso"
"logout": "Esci"
},
"settings": {
"title": "Impostazioni",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "La tua password attuale",
"passwordsDontMatch": "La nuova password e la conferma non coincidono.",
"passwordUpdateSuccess": "Password aggiornata con successo.",
"updateEmailTitle": "Aggiorna l'indirizzo e-mail",
"updateEmailTitle": "Inserisci il tuo indirizzo e-mail",
"updateEmailNew": "Nuovo indirizzo e-mail",
"updateEmailSuccess": "Il tuo indirizzo e-mail è stato aggiornato correttamente. Ti abbiamo inviato un collegamento per confermarlo.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Domenica",
"weekStartMonday": "Lunedì",
"language": "Lingua",
"defaultList": "Lista predefinita",
"timezone": "Fuso Orario"
"defaultList": "Lista predefinita"
},
"totp": {
"title": "Autenticazione a due fattori",
@ -103,39 +95,39 @@
"disableSuccess": "L'autenticazione a due fattori è stata disattivata."
},
"caldav": {
"title": "CalDAV",
"title": "CalDav",
"howTo": "Puoi connettere Vikunja ai client caldav per visualizzare e gestire tutte le attività da diversi client. Inserisci questo URL nel tuo client:",
"more": "Ulteriori informazioni su CalDAV in Vikunja"
"more": "Ulteriori informazioni su caldav in Vikunja"
},
"avatar": {
"title": "Avatar",
"initials": "Iniziali",
"gravatar": "Gravatar",
"marble": "Marmo",
"marble": "Marble",
"upload": "Carica",
"uploadAvatar": "Carica Avatar",
"statusUpdateSuccess": "Avatar aggiornato!",
"statusUpdateSuccess": "Avatar status was updated successfully!",
"setSuccess": "L'avatar è stato impostato con successo!"
},
"quickAddMagic": {
"title": "Modalità Aggiunta Rapida Magica",
"title": "Quick Add Magic Mode",
"disabled": "Disabilitato",
"todoist": "Todoist",
"vikunja": "Vikunja"
},
"appearance": {
"title": "Tema",
"setSuccess": "Tema cambiato in {colorScheme}",
"title": "Color Scheme",
"setSuccess": "Saved change of color scheme to {colorScheme}",
"colorScheme": {
"light": "Chiaro",
"system": "Sistema",
"dark": "Scuro"
"light": "Light",
"system": "System",
"dark": "Dark"
}
}
},
"deletion": {
"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.",
"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.",
"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.",
@ -149,7 +141,7 @@
},
"export": {
"title": "Esporta i tuoi dati Vikunja",
"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.",
"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.",
"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.",
@ -165,7 +157,7 @@
"searchSelect": "Fare clic o premere invio per selezionare questa lista",
"shared": "Liste Condivise",
"create": {
"header": "Nuova lista",
"header": "New list",
"titlePlaceholder": "Il titolo della lista va qui…",
"addTitleRequired": "Specifica un titolo.",
"createdSuccess": "La lista è stata creata correttamente.",
@ -199,7 +191,7 @@
"duplicate": {
"title": "Duplica questa lista",
"label": "Duplica",
"text": "Seleziona un namespace che dovrebbe contenere l'elenco duplicato:",
"text": "Select a namespace which should hold the duplicated list:",
"success": "Lista duplicata."
},
"edit": {
@ -287,23 +279,23 @@
"title": "Kanban",
"limit": "Limite: {limit}",
"noLimit": "Non Impostato",
"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à…",
"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…",
"addTask": "Aggiungi un'attività",
"addAnotherTask": "Aggiungi un'altra attività",
"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"
"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"
},
"pseudo": {
"favorites": {
@ -312,53 +304,52 @@
}
},
"namespace": {
"title": "Namespace e Liste",
"title": "Namespaces & Lists",
"namespace": "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…",
"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…",
"create": {
"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."
"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."
},
"archive": {
"titleArchive": "Archivia \"{namespace}\"",
"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.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "Se un namespace è archiviato, non è possibile creare nuove liste o modificarlo."
"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."
},
"delete": {
"title": "Elimina \"{namespace}\"",
"text1": "Sei sicuro di voler rimuovere questo namespace e tutto il relativo contenuto?",
"title": "Delete \"{namespace}\"",
"text1": "Are you sure you want to delete this namespace and all of its contents?",
"text2": "Questo include tutte le liste e le attività e NON PUÒ ESSERE RIPRISTINATO!",
"success": "Namespace eliminato."
"success": "The namespace was successfully deleted."
},
"edit": {
"title": "Modifica \"{namespace}\"",
"success": "Namespace aggiornato."
"success": "The namespace was successfully updated."
},
"share": {
"title": "Condividi \"{namespace}\""
},
"attributes": {
"title": "Titolo del Namespace",
"titlePlaceholder": "Il titolo del namespace va qui…",
"title": "Namespace Title",
"titlePlaceholder": "The namespace title goes here…",
"description": "Descrizione",
"descriptionPlaceholder": "La descrizione del namespace va qui…",
"descriptionPlaceholder": "The namespaces description goes here…",
"color": "Colore",
"archived": "Archiviato",
"isArchived": "Questo namespace è archiviato"
"archived": "Is Archived",
"isArchived": "This namespace is archived"
},
"pseudo": {
"sharedLists": {
@ -374,7 +365,7 @@
},
"filters": {
"title": "Filtri",
"clear": "Pulisci Filtri",
"clear": "Clear Filters",
"attributes": {
"title": "Titolo",
"titlePlaceholder": "Il titolo del filtro salvato va qui…",
@ -383,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": "Ordine alfabetico",
"sortAlphabetically": "Sort Alphabetically",
"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": "Intervallo date dei promemoria"
"reminderRange": "Reminder Date Range"
},
"create": {
"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.",
"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.",
"action": "Crea nuovo filtro salvato"
},
"delete": {
@ -455,9 +446,9 @@
},
"navigation": {
"overview": "Panoramica",
"upcoming": "Prossimamente",
"upcoming": "Upcoming",
"settings": "Impostazioni",
"imprint": "Informazioni legali",
"imprint": "Imprint",
"privacy": "Politica sulla Privacy"
},
"misc": {
@ -473,19 +464,19 @@
"searchPlaceholder": "Digita per cercare…",
"previous": "Precedente",
"next": "Successivo",
"poweredBy": "Creato con Vikunja",
"poweredBy": "Powered by Vikunja",
"info": "Info",
"create": "Crea",
"create": "Create",
"doit": "Fallo!",
"saving": "Salvataggio…",
"saved": "Salvato!",
"default": "Predefinito",
"close": "Chiudi",
"download": "Scarica",
"showMenu": "Mostra il menu",
"hideMenu": "Nascondi il menù",
"forExample": "Ad esempio:",
"welcomeBack": "Bentornato!"
"showMenu": "Show the menu",
"hideMenu": "Hide the menu",
"forExample": "For example:",
"welcomeBack": "Welcome Back!"
},
"input": {
"resetColor": "Ripristina Colore",
@ -494,9 +485,9 @@
"tomorrow": "Domani",
"nextMonday": "Lunedì Prossimo",
"thisWeekend": "Questo fine settimana",
"laterThisWeek": "Alla fine di questa settimana",
"laterThisWeek": "Later This Week",
"nextWeek": "Prossima Settimana",
"chooseDate": "Seleziona una data"
"chooseDate": "Choose a date"
},
"editor": {
"edit": "Modifica",
@ -513,16 +504,16 @@
"quote": "Citazione",
"unorderedList": "Elenco puntato",
"orderedList": "Elenco numerato",
"cleanBlock": "Pulisci Blocco",
"cleanBlock": "Clean Block",
"link": "Link",
"image": "Immagine",
"table": "Tabella",
"horizontalRule": "Divisore Orizzontale",
"sideBySide": "Affianca",
"guide": "Guida"
"horizontalRule": "Horizontal Rule",
"sideBySide": "Side By Side",
"guide": "Guide"
},
"multiselect": {
"createPlaceholder": "Crea nuovo",
"createPlaceholder": "Create new",
"selectPlaceholder": "Clicca o premere invio per selezionare"
}
},
@ -542,19 +533,19 @@
"titleDates": "Attività dal {from} al {to}",
"noDates": "Mostra attività senza date",
"current": "Attività attuali",
"from": "Attività dal",
"until": "fino al",
"from": "Tasks from",
"until": "until",
"today": "Oggi",
"nextWeek": "Settimana Prossima",
"nextMonth": "Prossimo Mese",
"noTasks": "Nessuna attività — Buona giornata!"
"noTasks": "Nothing to do — Have a nice day!"
},
"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": "Segna attività fatta!",
"done": "Mark task done!",
"undone": "Segna come non completato",
"created": "Creato {0} da {1}",
"updated": "Aggiornato {0}",
@ -563,21 +554,21 @@
"deleteSuccess": "L'attività è stata eliminata con successo.",
"belongsToList": "Questa attività appartiene alla lista '{list}'",
"due": "Scadenza {at}",
"closePopup": "Chiudi popup",
"closePopup": "Close 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": "Assegna ad un utente",
"assign": "Assign to a user",
"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": "Imposta ricorrenza",
"repeatAfter": "Set a repeating interval",
"percentDone": "Imposta Percentuale Completata",
"attachments": "Aggiungi allegati",
"relatedTasks": "Aggiungi attività collegate",
@ -608,13 +599,13 @@
"updated": "Aggiornato"
},
"subscription": {
"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}"
"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}"
},
"attachment": {
"title": "Allegati",
@ -632,41 +623,41 @@
"comment": {
"title": "Commenti",
"loading": "Caricamento commenti…",
"edited": "modificato il {date}",
"edited": "edited {date}",
"creating": "Creazione del commento…",
"placeholder": "Aggiungi un commento…",
"comment": "Commenta",
"comment": "Comment",
"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": "Rinvia data di scadenza",
"title": "Defer due date",
"1day": "1 giorno",
"3days": "3 giorni",
"1week": "1 settimana"
},
"description": {
"placeholder": "Clicca qui per inserire una descrizione…",
"empty": "Nessuna descrizione."
"placeholder": "Click here to enter a description…",
"empty": "No description available yet."
},
"assignee": {
"placeholder": "Digita per assegnare un utente…",
"placeholder": "Type to assign a user…",
"selectPlaceholder": "Assegna questo utente",
"assignSuccess": "Utente assegnato.",
"unassignSuccess": "Utente disassegnato."
"assignSuccess": "The user has been assigned successfully.",
"unassignSuccess": "The user has been unassigned successfully."
},
"label": {
"placeholder": "Digita per aggiungere una nuova etichetta…",
"createPlaceholder": "Aggiungila come nuova etichetta",
"placeholder": "Type to add a new label…",
"createPlaceholder": "Add this as new label",
"addSuccess": "Etichetta aggiunta.",
"createSuccess": "Etichetta creata.",
"removeSuccess": "Etichetta eliminata.",
"addCreateSuccess": "Etichetta creata e aggiunta."
},
"priority": {
"unset": "Azzera",
"unset": "Unset",
"low": "Bassa",
"medium": "Media",
"high": "Alta",
@ -674,38 +665,38 @@
"doNow": "FARE ORA"
},
"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à?",
"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?",
"deleteText2": "Questa azione non può essere annullata!",
"select": "Seleziona un tipo di collegamento",
"select": "Select a relation kind",
"kinds": {
"subtask": "Sotto-attività | Sotto-attività",
"parenttask": "Attività Principale | Attività Principale",
"related": "Attività Correlata | Attività Correlata",
"subtask": "Subtask | Subtasks",
"parenttask": "Parent Task | Parent Tasks",
"related": "Related Task | Related Tasks",
"duplicateof": "Duplicato Di | Duplicati Di",
"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"
"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"
}
},
"repeat": {
"everyDay": "Ogni Giorno",
"everyWeek": "Ogni Settimana",
"everyMonth": "Ogni Mese",
"mode": "Modalità Ripetizione",
"mode": "Repeat mode",
"monthly": "Mensilmente",
"fromCurrentDate": "Dalla Data Attuale",
"fromCurrentDate": "From Current Date",
"each": "Ogni",
"specifyAmount": "Specifica una quantità…",
"hours": "Ore",
@ -715,32 +706,32 @@
"years": "Anni"
},
"quickAddMagic": {
"hint": "Puoi usare l'Aggiunta Rapida Magica",
"hint": "You can use Quick Add Magic",
"what": "Cosa?",
"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.",
"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.",
"multiple": "Puoi usarlo più volte.",
"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.",
"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.",
"dateAndTime": "Data e ora",
"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à lanno 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)."
"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)."
}
},
"team": {
"title": "Gruppi",
"title": "Teams",
"noTeams": "Non fai parte di nessun gruppo.",
"create": {
"title": "Crea un nuovo gruppo",
@ -755,23 +746,23 @@
"makeAdmin": "Rendi Amministratore",
"success": "Gruppo aggiornato.",
"userAddedSuccess": "Membro del gruppo aggiunto.",
"madeMember": "Membro del gruppo reso membro.",
"madeAdmin": "Membro del gruppo reso amministratore.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"delete": {
"header": "Elimina il gruppo",
"text1": "Sei sicuro di voler eliminare questo gruppo e tutti i suoi membri?",
"text2": "Tutti i membri del gruppo perderanno l'accesso alle liste e ai namespace condivisi con questo gruppo. NON PUÒ ESSERE RIPRISTINATO!",
"text2": "All team members will lose access to lists and namespaces shared with this team. This CANNOT BE UNDONE!",
"success": "Gruppo eliminato."
},
"deleteUser": {
"header": "Rimuovi un utente dal gruppo",
"text1": "Confermi di voler rimuovere questo utente dal gruppo?",
"text2": "Perderanno l'accesso a tutte le liste e i namespace a cui questo gruppo ha accesso. NON PUÒ ESSERE RIPRISTINATO!",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "Utente rimosso dal gruppo."
}
},
"attributes": {
"name": "Nome Gruppo",
"name": "Team Name",
"namePlaceholder": "Il nome del gruppo va qui…",
"nameRequired": "Specifica un nome.",
"description": "Descrizione",
@ -781,32 +772,32 @@
}
},
"keyboardShortcuts": {
"title": "Tasti Rapidi",
"general": "Generali",
"title": "Keyboard Shortcuts",
"general": "General",
"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": "e dopo",
"then": "then",
"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"
"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"
},
"list": {
"title": "Viste Liste",
"switchToListView": "Passa alla vista Lista",
"switchToGanttView": "Passa alla vista Gantt",
"switchToKanbanView": "Passa alla vista Kanban",
"switchToTableView": "Passa alla vista Tabella"
"title": "List Views",
"switchToListView": "Switch to list view",
"switchToGanttView": "Switch to gantt view",
"switchToKanbanView": "Switch to kanban view",
"switchToTableView": "Switch to table view"
}
},
"update": {
"available": "È disponibile un aggiornamento per Vikunja!",
"available": "There is an update for Vikunja available!",
"do": "Aggiorna Adesso"
},
"menu": {
@ -814,136 +805,136 @@
"archive": "Archivia",
"duplicate": "Duplica",
"delete": "Elimina",
"unarchive": "Disarchivia",
"setBackground": "Imposta sfondo",
"unarchive": "Un-Archive",
"setBackground": "Set background",
"share": "Condividi",
"newList": "Nuova lista"
},
"apiConfig": {
"url": "URL Vikunja",
"urlPlaceholder": "es. http://localhost:8080",
"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."
"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."
},
"loadingError": {
"failed": "Caricamento non riuscito, si prega di {0}. Se l'errore persiste, per favore {1}.",
"tryAgain": "riprova",
"contact": "Contattaci"
"failed": "Loading failed, please {0}. If the error persists, please {1}.",
"tryAgain": "try again",
"contact": "contact us"
},
"notification": {
"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."
"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."
},
"quickActions": {
"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á",
"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",
"lists": "Liste",
"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})",
"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})",
"cmds": {
"newTask": "Nuova attività",
"newList": "Nuova lista",
"newNamespace": "Nuovo Namespace",
"newTeam": "Nuovo gruppo"
"newTask": "New task",
"newList": "New list",
"newNamespace": "New namespace",
"newTeam": "New team"
}
},
"date": {
"locale": "it",
"locale": "en",
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"error": {
"error": "Errore",
"success": "Fatto",
"success": "Success",
"0001": "Non ti è permesso farlo.",
"1001": "Esiste già un utente con questo nome utente.",
"1001": "A user with this username already exists.",
"1002": "Un utente con questo indirizzo e-mail esiste già.",
"1004": "Nessun nome utente e password specificati.",
"1004": "No username and password specified.",
"1005": "L'utente non esiste.",
"1006": "Impossibile ottenere l'id utente.",
"1008": "Nessun codice di reimpostazione password fornito.",
"1009": "Codice di reimpostazione password non valido.",
"1008": "No password reset token provided.",
"1009": "Invalid password reset token.",
"1010": "Token di conferma dell'e-mail non valido.",
"1011": "Nome utente o password errati.",
"1011": "Wrong username or password.",
"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": "L'impostazione del tipo di avatar utente non è valida.",
"1018": "The user avatar type setting is invalid.",
"2001": "L'ID non può essere vuoto o 0.",
"2002": "Alcuni dati della richiesta non erano validi.",
"3001": "La lista non esiste.",
"3004": "Devi avere i permessi di lettura su quella lista per eseguire quell'azione.",
"3004": "You need to have read permissions on that list to perform that action.",
"3005": "Il titolo della lista non può essere vuoto.",
"3006": "La condivisione della lista non esiste.",
"3006": "The list share does not exist.",
"3007": "Esiste già una lista con questo identificatore.",
"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.",
"3008": "The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list.",
"4001": "The list task text cannot be empty.",
"4002": "The list task does not exist.",
"4003": "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": "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.",
"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 concatenator.",
"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.",
"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": "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."
"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."
},
"about": {
"title": "Informazioni",
"frontendVersion": "Versione Frontend: {version}",
"apiVersion": "Versione API: {version}"
"title": "About",
"frontendVersion": "Frontend Version: {version}",
"apiVersion": "API Version: {version}"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,949 +0,0 @@
{
"home": {
"welcomeNight": "Dobrej nocy {username}",
"welcomeMorning": "Dzień dobry {username}",
"welcomeDay": "Cześć {username}",
"welcomeEvening": "Dobry wieczór {username}",
"lastViewed": "Ostatnio oglądane",
"list": {
"newText": "Możesz stworzyć nową listę dla swoich nowych zadań:",
"new": "Nowa lista",
"importText": "Lub zaimportować swoje listy i zadania z innych usług do Vikunja:",
"import": "Zaimportuj swoje dane do Vikunja"
}
},
"404": {
"title": "Nie znaleziono",
"text": "Żądana strona nie istnieje."
},
"ready": {
"loading": "Vikunja się ładuje…",
"errorOccured": "Wystąpił błąd:",
"checkApiUrl": "Sprawdź, czy adres URL interfejsu API jest poprawny.",
"noApiUrlConfigured": "Nie skonfigurowano adresu URL interfejsu API. Ustaw go poniżej:"
},
"offline": {
"title": "Jesteś offline.",
"text": "Sprawdź połączenie internetowe i spróbuj ponownie."
},
"user": {
"auth": {
"username": "Nazwa użytkownika",
"usernameEmail": "Nazwa użytkownika lub adres e-mail",
"usernamePlaceholder": "np. frederick",
"email": "Adres e-mail",
"emailPlaceholder": "np. frederic{'@'}vikunja.io",
"password": "Hasło",
"passwordPlaceholder": "np. •••••••••••",
"forgotPassword": "Zapomniałeś hasła?",
"resetPassword": "Zresetuj swoje hasło",
"resetPasswordAction": "Wyślij mi link do resetowania hasła",
"resetPasswordSuccess": "Sprawdź swoją skrzynkę odbiorczą! Powinieneś otrzymać e-mail z instrukcją, jak zresetować hasło.",
"passwordsDontMatch": "Hasła nie są takie same",
"confirmEmailSuccess": "Pomyślnie zatwierdzono e-mail! Możesz się teraz zalogować.",
"totpTitle": "Kod uwierzytelniania dwuskładnikowego",
"totpPlaceholder": "np. 123456",
"login": "Zaloguj sie",
"createAccount": "Utwórz konto",
"loginWith": "Zaloguj się przez {provider}",
"authenticating": "Uwierzytelnianie…",
"openIdStateError": "Stan się nie zgadza, odmowa kontynuacji!",
"openIdGeneralError": "Wystąpił błąd podczas uwierzytelniania wobec strony trzeciej.",
"logout": "Wyloguj",
"emailInvalid": "Proszę podać poprawny adres e-mail.",
"usernameRequired": "Proszę podać nazwę użytkownika.",
"passwordRequired": "Proszę podać hasło.",
"showPassword": "Pokaż hasło",
"hidePassword": "Ukryj hasło",
"noAccountYet": "Nie masz jeszcze konta?",
"alreadyHaveAnAccount": "Masz już konto?",
"remember": "Pozostań zalogowany"
},
"settings": {
"title": "Ustawienia",
"newPasswordTitle": "Zaktualizuj swoje hasło",
"newPassword": "Nowe hasło",
"newPasswordConfirm": "Potwierdź hasło",
"currentPassword": "Aktualne hasło",
"currentPasswordPlaceholder": "Twoje aktualne hasło",
"passwordsDontMatch": "Nowe hasło i jego potwierdzenie nie są takie same.",
"passwordUpdateSuccess": "Hasło zostało pomyślnie zaktualizowane.",
"updateEmailTitle": "Zaktualizuj swój adres e-mail",
"updateEmailNew": "Nowy adres e-mail",
"updateEmailSuccess": "Twój adres e-mail został pomyślnie zaktualizowany. Wysłaliśmy Ci link do potwierdzenia operacji.",
"general": {
"title": "Ustawienia główne",
"name": "Nazwa",
"newName": "Nowa nazwa",
"savedSuccess": "Ustawienia zostały pomyślnie zaktualizowane.",
"emailReminders": "Wysyłaj mi przypomnienia o zadaniach przez e-mail",
"overdueReminders": "Codziennie rano wysyłaj mi przypomnienia o zaległych niewykonanych zadaniach e-mailem",
"discoverableByName": "Pozwól innym użytkownikom znaleźć mnie, gdy będą szukać mojej nazwy",
"discoverableByEmail": "Pozwól innym użytkownikom znaleźć mnie, gdy będą szukać mojego adresu e-mail",
"playSoundWhenDone": "Odtwórz dźwięk podczas oznaczania zadań jako ukończonych",
"weekStart": "Tydzień zaczyna się od",
"weekStartSunday": "niedzieli",
"weekStartMonday": "poniedziałku",
"language": "Język",
"defaultList": "Domyślna lista",
"timezone": "Strefa czasowa"
},
"totp": {
"title": "Uwierzytelnianie dwuskładnikowe",
"enroll": "Włącz",
"finishSetupPart1": "Aby dokończyć, użyj tego kodu konfiguracyjnego w swojej aplikacji TOTP (Google Authenticator lub podobnej):",
"finishSetupPart2": "Następnie przepisz poniżej kod autoryzacyjny ze swojej aplikacji.",
"scanQR": "Możesz również zeskanować ten kod QR:",
"passcode": "Kod autoryzacyjny",
"passcodePlaceholder": "Kod wygenerowany przez twoją aplikację TOTP",
"setupSuccess": "Udało Ci się skonfigurować uwierzytelnianie dwuskładnikowe!",
"enterPassword": "Wprowadź hasło",
"disable": "Wyłącz uwierzytelnianie dwuskładnikowe",
"confirmSuccess": "Pomyślnie skonfigurowałeś uwierzytelnianie dwuskładnikowe i od teraz możesz z niego korzystać!",
"disableSuccess": "Uwierzytelnianie dwuskładnikowe zostało pomyślnie wyłączone."
},
"caldav": {
"title": "Caldav",
"howTo": "Możesz połączyć Vikunja z klientami caldav, aby przeglądać i zarządzać wszystkimi zadaniami z różnych klientów. Wprowadź ten adres URL do swojego klienta:",
"more": "Więcej informacji o CalDAV w Vikunji"
},
"avatar": {
"title": "Awatar",
"initials": "Inicjały",
"gravatar": "Gravatar",
"marble": "Szklana kulka",
"upload": "Prześlij",
"uploadAvatar": "Prześlij awatar",
"statusUpdateSuccess": "Status awatara został pomyślnie zaktualizowany!",
"setSuccess": "Awatar został pomyślnie ustawiony!"
},
"quickAddMagic": {
"title": "Tryb Quick Add Magic",
"disabled": "Wyłączony",
"todoist": "Todoist",
"vikunja": "Vikunja"
},
"appearance": {
"title": "Schemat kolorów",
"setSuccess": "Zapisano zmianę schematu kolorów na {colorScheme}",
"colorScheme": {
"light": "Jasny",
"system": "Systemowy",
"dark": "Ciemny"
}
}
},
"deletion": {
"title": "Usuń swoje konto Vikunja",
"text1": "Usunięcie Twojego konta jest trwałe i nie można tego cofnąć. Usuniemy wszystkie Twoje sekcje, listy, zadania i wszystko, co z nimi powiązane.",
"text2": "Aby kontynuować, wprowadź swoje hasło. Otrzymasz wiadomość e-mail z dalszymi instrukcjami.",
"confirm": "Usuń moje konto",
"requestSuccess": "Żądanie powiodło się. Otrzymasz wiadomość e-mail z dalszymi instrukcjami.",
"passwordRequired": "Wprowadź hasło.",
"confirmSuccess": "Pomyślnie potwierdziłeś usunięcie swojego konta. Za trzy dni usuniemy Twoje konto.",
"scheduled": "Twoje konto Vikunja zostanie usunięte w dniu {date} ({dateSince}).",
"scheduledCancel": "Aby anulować usunięcie konta, kliknij tutaj.",
"scheduledCancelText": "Aby anulować usunięcie konta, wprowadź poniżej swoje hasło:",
"scheduledCancelConfirm": "Anuluj usunięcie mojego konta",
"scheduledCancelSuccess": "Nie usuniemy Twojego konta."
},
"export": {
"title": "Eksportuj swoje dane Vikunja",
"description": "Możesz zażądać kopii wszystkich swoich danych Vikunja. Obejmuje to sekcje, listy, zadania i wszystko, co z nimi powiązane. Możesz zaimportować te dane do dowolnej instancji Vikunja za pomocą funkcji migracji.",
"descriptionPasswordRequired": "Wprowadź hasło, aby kontynuować:",
"request": "Generuj kopię moich danych Vikunja",
"success": "Pomyślnie zażądałeś danych Vikunja! Wyślemy Ci e-mail, gdy będą gotowe do pobrania.",
"downloadTitle": "Pobierz wyeksportowane dane Vikunja"
}
},
"list": {
"archived": "Ta lista jest zarchiwizowana. Nie można w niej tworzyć ani edytować zadań.",
"title": "Tytuł listy",
"color": "Kolor",
"lists": "Listy",
"search": "Wpisz, aby wyszukać listę…",
"searchSelect": "Kliknij lub naciśnij Enter, aby wybrać tę listę",
"shared": "Współdzielone listy",
"create": {
"header": "Nowa lista",
"titlePlaceholder": "Tu wpisz tytuł listy…",
"addTitleRequired": "Proszę podaj tytuł.",
"createdSuccess": "Lista została pomyślnie utworzona.",
"addListRequired": "Proszę, wybierz listę lub ustaw listę domyślną w ustawieniach."
},
"archive": {
"title": "Archiwizuj \"{list}\"",
"archive": "Zarchiwizuj tę listę",
"unarchive": "Cofnij archiwizację tej listy",
"unarchiveText": "Będziesz mógł tworzyć nowe zadania lub je edytować.",
"archiveText": "Nie będziesz mógł edytować tej listy ani tworzyć nowych zadań, dopóki nie cofniesz jej archiwizacji.",
"success": "Lista została pomyślnie zarchiwizowana."
},
"background": {
"title": "Ustaw tło listy",
"remove": "Usuń tło",
"upload": "Prześlij tło ze swojego komputera",
"searchPlaceholder": "Wyszukiwanie tła…",
"poweredByUnsplash": "Wspierane przez Unsplash",
"loadMore": "Załaduj więcej zdjęć",
"success": "Tło zostało ustawione pomyślnie!",
"removeSuccess": "Tło zostało pomyślnie usunięte!"
},
"delete": {
"title": "Usuń \"{list}\"",
"header": "Usuń tę listę",
"text1": "Czy na pewno chcesz usunąć tę listę i całą jej zawartość?",
"text2": "Obejmuje to wszystkie zadania i tego NIE DA SIĘ COFNĄĆ!",
"success": "Lista została pomyślnie usunięta."
},
"duplicate": {
"title": "Duplikuj tę listę",
"label": "Duplikuj",
"text": "Wybierz sekcję, do której powinna trafić zduplikowana lista:",
"success": "Lista została pomyślnie zduplikowana."
},
"edit": {
"header": "Edytuj tę listę",
"title": "Edytuj \"{list}\"",
"titlePlaceholder": "Tu wpisz tytuł listy…",
"identifierTooltip": "Identyfikator listy może być użyty do jednoznacznej identyfikacji zadania z różnych list. Możesz ustawić go jako pusty, aby go wyłączyć.",
"identifier": "Identyfikator listy",
"identifierPlaceholder": "Tu wpisz identyfikator listy…",
"description": "Opis",
"descriptionPlaceholder": "Tu wpisz opis listy…",
"color": "Kolor",
"success": "Lista została pomyślnie zaktualizowana."
},
"share": {
"header": "Udostępnij tę listę",
"title": "Udostępnij \"{list}\"",
"share": "Udostępnij",
"links": {
"title": "Udostępnij linki",
"what": "Co to jest udostępnianie linków?",
"explanation": "Udostępnianie linków umożliwia łatwe udostępnianie listy innym użytkownikom, którzy nie mają konta na Vikunja.",
"create": "Utwórz nowy link do udostępnienia",
"name": "Nazwa: (opcjonalnie)",
"namePlaceholder": "np. Lorem Ipsum",
"nameExplanation": "Wszystkie działania wykonane przez ten link będą wyświetlane pod tą nazwą.",
"password": "Hasło (opcjonalnie)",
"passwordExplanation": "Podczas uwierzytelniania użytkownik będzie musiał wprowadzić to hasło.",
"noName": "Nie ustawiono nazwy",
"remove": "Usuń link",
"removeText": "Czy na pewno chcesz usunąć ten link? Dostęp do tej listy z tym linkiem nie będzie już możliwy. Tego nie da się cofnąć!",
"createSuccess": "Pomyślnie utworzono udostępniony link.",
"deleteSuccess": "Udostępniony link został pomyślnie usunięty"
},
"userTeam": {
"typeUser": "użytkownik(a) | użytkownikom",
"typeTeam": "zespół | zespołom",
"shared": "Udostępniono tym {type}",
"you": "Ty",
"notShared": "Nie udostępniono jeszcze żadnym {type}.",
"removeHeader": "Usuń {type} z {sharable}",
"removeText": "Czy na pewno chcesz usunąć {type} z {sharable}? Tego nie da się cofnąć!",
"removeSuccess": "{type} został pomyślnie usunięty z {sharable}.",
"addedSuccess": "{type} został pomyślnie dodany.",
"updatedSuccess": "{type} został pomyślnie dodany."
},
"right": {
"title": "Uprawnienia",
"read": "Tylko do odczytu",
"readWrite": "Odczyt i zapis",
"admin": "Administrator"
},
"attributes": {
"link": "Link",
"name": "Nazwa",
"sharedBy": "Udostępniony przez",
"right": "Uprawnienia",
"delete": "Usuń"
}
},
"list": {
"title": "Lista",
"add": "Dodaj",
"addPlaceholder": "Dodaj nowe zadanie…",
"empty": "Ta lista jest obecnie pusta.",
"newTaskCta": "Utwórz nowe zadanie.",
"editTask": "Edytuj zadanie"
},
"gantt": {
"title": "Gantt",
"showTasksWithoutDates": "Pokaż zadania, które nie mają ustawionych dat",
"size": "Rozmiar",
"default": "Domyślny",
"month": "Miesiąc",
"day": "Dzień",
"from": "Od",
"to": "Do",
"noDates": "To zadanie nie ma ustawionych dat."
},
"table": {
"title": "Tabela",
"columns": "Kolumny"
},
"kanban": {
"title": "Kanban",
"limit": "Limit: {limit}",
"noLimit": "Nie ustawiony",
"doneBucket": "Zasobnik ukończonych zadań",
"doneBucketHint": "Wszystkie zadania przeniesione do tego zasobnika zostaną automatycznie oznaczone jako ukończone.",
"doneBucketHintExtended": "Wszystkie zadania przeniesione do zasobnika ukończonych zadań zostaną automatycznie oznaczone jako ukończone. Również wszystkie zadania z innych zasobników oznaczone jako ukończone zostaną do niego przeniesione.",
"doneBucketSavedSuccess": "Zasobnik ukończonych zadań został pomyślnie zapisany.",
"deleteLast": "Nie możesz usunąć ostatniego zasobnika.",
"addTaskPlaceholder": "Wpisz tytuł nowego zadania…",
"addTask": "Dodaj zadanie",
"addAnotherTask": "Dodaj kolejne zadanie",
"addBucket": "Utwórz nowy zasobnik",
"addBucketPlaceholder": "Wpisz tytuł nowego zasobnika…",
"deleteHeaderBucket": "Usuń zasobnik",
"deleteBucketText1": "Czy na pewno chcesz usunąć ten zasobnik?",
"deleteBucketText2": "Nie spowoduje to usunięcia żadnych zadań, ale przeniesie je do domyślnego zasobnika.",
"deleteBucketSuccess": "Zasobnik został pomyślnie usunięty.",
"bucketTitleSavedSuccess": "Tytuł zasobnika został pomyślnie zapisany.",
"bucketLimitSavedSuccess": "Limit zasobnika został pomyślnie zapisany.",
"collapse": "Zwiń ten zasobnik"
},
"pseudo": {
"favorites": {
"title": "Ulubione"
}
}
},
"namespace": {
"title": "Sekcje i Listy",
"namespace": "Sekcja",
"showArchived": "Pokaż zarchiwizowane",
"noneAvailable": "W tej chwili nie masz żadnych sekcji.",
"unarchive": "Cofnij archiwizację",
"archived": "Zarchiwizowane",
"noLists": "Ta sekcja nie zawiera żadnych list.",
"createList": "Utwórz nową listę w tej sekcji.",
"namespaces": "Sekcje",
"search": "Wpisz, aby wyszukać sekcję…",
"create": {
"title": "Nowa sekcja",
"titleRequired": "Proszę, podaj tytuł.",
"explanation": "Sekcja to zbiór list, które możesz udostępniać i używać do organizowania list. Każda lista należy do jakiejś sekcji.",
"tooltip": "Co to jest sekcja?",
"success": "Sekcja została pomyślnie utworzona."
},
"archive": {
"titleArchive": "Archiwizuj \"{namespace}\"",
"titleUnarchive": "Cofnij archiwizację \"{namespace}\"",
"archiveText": "Nie będziesz mógł tworzyć nowych list ani edytować tej sekcji, dopóki nie cofniesz archiwizacji. Ta operacja zarchiwizuje również wszystkie listy należące do tej sekcji.",
"unarchiveText": "Będziesz mógł tworzyć nowe listy i je edytować.",
"success": "Sekcja została pomyślnie zarchiwizowana.",
"unarchiveSuccess": "Archiwizacja sekcji została pomyślnie cofnięta.",
"description": "Jeśli sekcja jest zarchiwizowana, nie można edytować ani tworzyć nowych list."
},
"delete": {
"title": "Usuń \"{namespace}\"",
"text1": "Czy na pewno chcesz usunąć tę sekcję i całą jej zawartość?",
"text2": "Dotyczy to wszystkich list i zadań i tego NIE DA SIĘ COFNĄĆ!",
"success": "Sekcja została pomyślnie usunięta."
},
"edit": {
"title": "Edytuj \"{namespace}\"",
"success": "Sekcja została pomyślnie zaktualizowana."
},
"share": {
"title": "Udostępnij \"{namespace}\""
},
"attributes": {
"title": "Tytuł sekcji",
"titlePlaceholder": "Tu wpisz tytuł sekcji…",
"description": "Opis",
"descriptionPlaceholder": "Tu wpisz opis sekcji…",
"color": "Kolor",
"archived": "Archiwizacja",
"isArchived": "Ta sekcja jest zarchiwizowana"
},
"pseudo": {
"sharedLists": {
"title": "Współdzielone listy"
},
"favorites": {
"title": "Ulubione"
},
"savedFilters": {
"title": "Filtry"
}
}
},
"filters": {
"title": "Filtry",
"clear": "Wyczyść filtry",
"attributes": {
"title": "Tytuł",
"titlePlaceholder": "Tu wpisz tytuł filtra stałego…",
"description": "Opis",
"descriptionPlaceholder": "Tu wpisz opis…",
"includeNulls": "Uwzględnij zadania, które nie mają ustawionej danej wartości",
"requireAll": "Wymagaj, aby wszystkie filtry były spełnione, aby zadanie się pojawiło",
"showDoneTasks": "Pokaż ukończone zadania",
"sortAlphabetically": "Sortuj alfabetycznie",
"enablePriority": "Włącz filtrowanie według priorytetu",
"enablePercentDone": "Włącz filtrowanie według procentu ukończenia",
"dueDateRange": "Zakres daty terminu",
"startDateRange": "Zakres daty rozpoczęcia",
"endDateRange": "Zakres daty zakończenia",
"reminderRange": "Zakres daty przypomnienia"
},
"create": {
"title": "Nowy filtr stały",
"description": "Filtr stały to wirtualna lista, która jest kalkulowana na podstawie zestawu filtrów przy każdym wejściu w nią. Po utworzeniu pojawi się w specjalnej sekcji.",
"action": "Utwórz nowy filtr stały"
},
"delete": {
"header": "Usuń ten filtr stały",
"text": "Czy na pewno chcesz usunąć ten filtr stały?",
"success": "Filtr został pomyślnie usunięty."
},
"edit": {
"title": "Edytuj ten filtr stały",
"success": "Filtr został pomyślnie zapisany."
}
},
"migrate": {
"title": "Migruj z innych usług do Vikunja",
"titleService": "Zaimportuj swoje dane z {name} do Vikunja",
"import": "Zaimportuj swoje dane do Vikunja",
"description": "Aby rozpocząć, kliknij logo jednej z usług zewnętrznych.",
"descriptionDo": "Vikunja zaimportuje wszystkie listy, zadania, notatki, przypomnienia i pliki, do których masz dostęp.",
"authorize": "Aby zezwolić Vikunja na dostęp do Twojego konta {name}, kliknij przycisk poniżej.",
"getStarted": "Rozpocznij",
"inProgress": "Trwa importowanie…",
"alreadyMigrated1": "Wygląda na to, że zaimportowałeś już swoje komponenty z {name} w dniu {date}.",
"alreadyMigrated2": "Ponowne importowanie jest możliwe, ale proces ten utworzy duplikaty. Jesteś pewny?",
"confirm": "Jestem pewien, proszę rozpocząć migrację!",
"importUpload": "Aby zaimportować dane z {name} do Vikunja, kliknij przycisk poniżej, aby wybrać plik.",
"upload": "Prześlij plik"
},
"label": {
"title": "Etykiety",
"manage": "Zarządzaj etykietami",
"description": "Kliknij etykietę, aby ją edytować. Możesz edytować wszystkie etykiety które utworzyłeś. Możesz edytować każdą etykietę powiązaną z zadaniem, do którego masz dostęp.",
"newCTA": "Obecnie nie masz żadnych etykiet.",
"search": "Wpisz, aby wyszukać etykietę…",
"create": {
"header": "Nowa etykieta",
"title": "Utwórz nową etykietę",
"titleRequired": "Proszę, podaj tytuł.",
"success": "Etykieta została pomyślnie utworzona."
},
"edit": {
"header": "Edytuj etykietę",
"forbidden": "Nie możesz edytować tej etykiety, ponieważ nie jesteś jej właścicielem.",
"success": "Etykieta została pomyślnie zaktualizowana."
},
"deleteSuccess": "Etykieta została pomyślnie usunięta.",
"attributes": {
"title": "Tytuł",
"titlePlaceholder": "Tu wpisz tytuł etykiety…",
"description": "Opis",
"descriptionPlaceholder": "Opis etykiety",
"color": "Kolor"
}
},
"sharing": {
"authenticating": "Uwierzytelnianie…",
"passwordRequired": "Ta współdzielona lista wymaga hasła. Wpisz je poniżej:",
"error": "Wystąpił błąd.",
"invalidPassword": "Hasło jest nieprawidłowe."
},
"navigation": {
"overview": "Przegląd",
"upcoming": "Nadchodzące",
"settings": "Ustawienia",
"imprint": "Odcisk",
"privacy": "Polityka prywatności"
},
"misc": {
"loading": "Ładowanie…",
"save": "Zapisz",
"delete": "Usuń",
"confirm": "Potwierdź",
"cancel": "Anuluj",
"refresh": "Odśwież",
"disable": "Wyłącz",
"copy": "Skopiuj do schowka",
"search": "Szukaj",
"searchPlaceholder": "Wpisz, aby wyszukać…",
"previous": "Poprzedni",
"next": "Następny",
"poweredBy": "Wspierane przez Vikunja",
"info": "Informacje",
"create": "Utwórz",
"doit": "Zrób to!",
"saving": "Zapisywanie…",
"saved": "Zapisano!",
"default": "Domyślnie",
"close": "Zamknij",
"download": "Pobierz",
"showMenu": "Pokaż menu",
"hideMenu": "Ukryj menu",
"forExample": "Na przykład:",
"welcomeBack": "Witaj ponownie!"
},
"input": {
"resetColor": "Resetuj kolor",
"datepicker": {
"today": "Dziś",
"tomorrow": "Jutro",
"nextMonday": "Następny poniedziałek",
"thisWeekend": "W ten weekend",
"laterThisWeek": "Pod koniec tego tygodnia",
"nextWeek": "W następnym tygodniu",
"chooseDate": "Wybierz datę"
},
"editor": {
"edit": "Edytuj",
"done": "Gotowe",
"heading1": "Nagłówek 1",
"heading2": "Nagłówek 2",
"heading3": "Nagłówek 3",
"headingSmaller": "Nagłówek mniejszy",
"headingBigger": "Nagłówek większy",
"bold": "Pogrubiony",
"italic": "Kursywa",
"strikethrough": "Przekreślony",
"code": "Kod",
"quote": "Cytat",
"unorderedList": "Lista nieuporządkowana",
"orderedList": "Lista uporządkowana",
"cleanBlock": "Wyczyść blok",
"link": "Link",
"image": "Obraz",
"table": "Tabela",
"horizontalRule": "Linia pozioma",
"sideBySide": "Obok siebie",
"guide": "Przewodnik"
},
"multiselect": {
"createPlaceholder": "Utwórz nowy",
"selectPlaceholder": "Kliknij lub naciśnij Enter, aby wybrać"
}
},
"task": {
"task": "Zadanie",
"new": "Utwórz nowe zadanie",
"delete": "Usuń to zadanie",
"createSuccess": "Zadanie zostało pomyślnie utworzone.",
"addReminder": "Dodaj nowe przypomnienie…",
"doneSuccess": "Zadanie zostało pomyślnie oznaczone jako ukończone.",
"undoneSuccess": "Zadanie zostało pomyślnie otwarte ponownie.",
"openDetail": "Otwórz szczegółowy widok zadania",
"checklistTotal": "{checked} z {total} zadań",
"checklistAllDone": "{total} zadań",
"show": {
"titleCurrent": "Bieżące zadania",
"titleDates": "Zadania od {from} do {to}",
"noDates": "Pokaż zadania bez dat",
"current": "Bieżące zadania",
"from": "Zadania od",
"until": "do",
"today": "Dziś",
"nextWeek": "Następny tydzień",
"nextMonth": "Następny miesiąc",
"noTasks": "Brak zadań do wykonania miłego dnia!"
},
"detail": {
"chooseDueDate": "Kliknij tutaj, aby ustawić termin",
"chooseStartDate": "Kliknij tutaj, aby ustawić datę rozpoczęcia",
"chooseEndDate": "Kliknij tutaj, aby ustawić datę zakończenia",
"move": "Przenieś zadanie na inną listę",
"done": "Oznacz zadanie jako ukończone!",
"undone": "Otwórz zadanie ponownie",
"created": "Utworzono {0} przez {1}",
"updated": "Zaktualizowano {0}",
"doneAt": "Ukończone {0}",
"updateSuccess": "Zadanie zostało pomyślnie zapisane.",
"deleteSuccess": "Zadanie zostało pomyślnie usunięte.",
"belongsToList": "To zadanie należy do listy '{list}'",
"due": "Termin {at}",
"closePopup": "Zamknij okno",
"delete": {
"header": "Usuń to zadanie",
"text1": "Czy na pewno chcesz usunąć to zadanie?",
"text2": "Spowoduje to również usunięcie wszystkich załączników, przypomnień i powiązań z tym zadaniem i nie będzie można tego cofnąć!"
},
"actions": {
"assign": "Przypisz do użytkownika",
"label": "Dodaj etykiety",
"priority": "Ustaw priorytet",
"dueDate": "Ustaw termin",
"startDate": "Ustaw datę rozpoczęcia",
"endDate": "Ustaw datę zakończenia",
"reminders": "Ustaw przypomnienia",
"repeatAfter": "Ustaw interwał powtarzania",
"percentDone": "Ustaw procent ukończenia",
"attachments": "Dodaj załączniki",
"relatedTasks": "Dodaj powiązane zadania",
"moveList": "Przenieś zadanie",
"color": "Ustaw kolor zadania",
"delete": "Usuń zadanie",
"favorite": "Zapisz jako ulubione",
"unfavorite": "Usuń z ulubionych"
}
},
"attributes": {
"assignees": "Przypisani",
"color": "Kolor",
"created": "Utworzone",
"createdBy": "Utworzone przez",
"description": "Opis",
"done": "Ukończone",
"dueDate": "Termin",
"endDate": "Data zakończenia",
"labels": "Etykiety",
"percentDone": "% ukończenia",
"priority": "Priorytet",
"relatedTasks": "Powiązane zadania",
"reminders": "Przypomnienia",
"repeat": "Powtarzanie",
"startDate": "Data rozpoczęcia",
"title": "Tytuł",
"updated": "Zaktualizowano"
},
"subscription": {
"subscribedThroughParent": "Nie możesz zrezygnować z subskrypcji, ponieważ subskrybujesz {entity} za pośrednictwem {parent}.",
"subscribed": "Obecnie subskrybujesz {entity} i będziesz otrzymywać powiadomienia o zmianach.",
"notSubscribed": "Nie subskrybujesz {entity} i nie będziesz otrzymywać powiadomień o zmianach.",
"subscribe": "Subskrybuj",
"unsubscribe": "Anuluj subskrypcję",
"subscribeSuccess": "Od teraz subskrybujesz {entity}",
"unsubscribeSuccess": "Już nie subskrybujesz {entity}"
},
"attachment": {
"title": "Załączniki",
"createdBy": "utworzony {0} przez {1}",
"downloadTooltip": "Pobierz ten załącznik",
"upload": "Prześlij załącznik",
"drop": "Upuść pliki tutaj, aby przesłać",
"delete": "Usuń załącznik",
"deleteTooltip": "Usuń ten załącznik",
"deleteText1": "Czy na pewno chcesz usunąć załącznik {filename}?",
"deleteText2": "Tego nie da się cofnąć!",
"copyUrl": "Kopiuj URL",
"copyUrlTooltip": "Skopiuj adres URL tego załącznika do użycia w tekście"
},
"comment": {
"title": "Komentarze",
"loading": "Ładowanie komentarzy…",
"edited": "edytowane {date}",
"creating": "Tworzenie komentarza…",
"placeholder": "Dodaj swój komentarz…",
"comment": "Skomentuj",
"delete": "Usuń ten komentarz",
"deleteText1": "Czy na pewno chcesz usunąć ten komentarz?",
"deleteText2": "Tego nie da się cofnąć!",
"addedSuccess": "Komentarz został pomyślnie dodany."
},
"deferDueDate": {
"title": "Odroczenie terminu",
"1day": "1 dzień",
"3days": "3 dni",
"1week": "1 tydzień"
},
"description": {
"placeholder": "Kliknij tutaj, aby wprowadzić opis…",
"empty": "Nie ma jeszcze opisu."
},
"assignee": {
"placeholder": "Wpisz, aby przypisać użytkownika…",
"selectPlaceholder": "Przypisz tego użytkownika",
"assignSuccess": "Użytkownik został pomyślnie przypisany.",
"unassignSuccess": "Użytkownik został pomyślnie usunięty."
},
"label": {
"placeholder": "Wpisz, aby dodać nową etykietę…",
"createPlaceholder": "Dodaj jako nową etykietę",
"addSuccess": "Etykieta została pomyślnie dodana.",
"createSuccess": "Etykieta została pomyślnie utworzona.",
"removeSuccess": "Etykieta została pomyślnie usunięta.",
"addCreateSuccess": "Etykieta została pomyślnie utworzona i dodana."
},
"priority": {
"unset": "Nie ustawiony",
"low": "Niski",
"medium": "Średni",
"high": "Wysoki",
"urgent": "Pilny",
"doNow": "ZRÓB TO TERAZ"
},
"relation": {
"add": "Dodaj nowe powiązane zadanie",
"new": "Nowe powiązane zadanie",
"searchPlaceholder": "Wpisz, aby wyszukać zadanie, które chcesz dodać jako powiązane…",
"createPlaceholder": "Dodaj jako nowe powiązane zadanie",
"differentList": "To zadanie należy do innej listy.",
"differentNamespace": "To zadanie należy do innej sekcji.",
"noneYet": "Nie ma jeszcze powiązanych zadań.",
"delete": "Usuń powiązane zadanie",
"deleteText1": "Czy na pewno chcesz usunąć to powiązane zadanie?",
"deleteText2": "Tego nie da się cofnąć!",
"select": "Wybierz rodzaj powiązanego zadania",
"kinds": {
"subtask": "Podzadanie | Podzadania",
"parenttask": "Zadanie nadrzędne | Zadania nadrzędne",
"related": "Powiązane zadanie | Powiązane zadania",
"duplicateof": "Duplikat | Duplikaty",
"duplicates": "Duplikat | Duplikaty",
"blocking": "Blokujący | Blokujące",
"blocked": "Blokowany przez | Blokowane przez",
"precedes": "Poprzedzający | Poprzedzające",
"follows": "Wynikający z | Wynikające z",
"copiedfrom": "Skopiowany z | Skopiowane z",
"copiedto": "Skopiowany do | Skopiowane do"
}
},
"repeat": {
"everyDay": "Codziennie",
"everyWeek": "Co tydzień",
"everyMonth": "Co miesiąc",
"mode": "Tryb powtarzania",
"monthly": "Miesięczny",
"fromCurrentDate": "Od bieżącej daty",
"each": "Co",
"specifyAmount": "Określ ilość…",
"hours": "Godziny",
"days": "Dni",
"weeks": "Tygodnie",
"months": "Miesiące",
"years": "Lata"
},
"quickAddMagic": {
"hint": "Możesz użyć Quick Add Magic",
"what": "Co?",
"title": "Quick Add Magic",
"intro": "Podczas tworzenia zadania możesz użyć specjalnych słów kluczowych, aby bezpośrednio dodać atrybuty do nowo utworzonego zadania. Pozwala to na znacznie szybsze dodawanie powszechnie używanych atrybutów do zadań.",
"multiple": "Możesz użyć tego wielokrotnie.",
"label1": "Aby dodać etykietę, po prostu poprzedź nazwę etykiety przedrostkiem {prefix}.",
"label2": "Vikunja najpierw sprawdzi, czy etykieta już istnieje, a jeśli nie, utworzy ją.",
"label3": "Aby użyć spacji, po prostu podaj nazwę etykiety pomiędzy \".",
"label4": "Na przykład: {prefix}\"Etykieta ze spacjami\".",
"priority1": "Aby ustawić priorytet zadania, dodaj liczbę 1-5 poprzedzoną {prefix}.",
"priority2": "Im większa liczba, tym wyższy priorytet.",
"assignees": "Aby bezpośrednio przypisać zadanie użytkownikowi, dodaj nazwę użytkownika poprzedzoną przedrostkiem {prefix} do zadania.",
"list1": "Aby ustawić listę, do której ma zostać przypisane zadanie, wprowadź jego nazwę z prefiksem {prefix}.",
"list2": "Jeśli lista nie istnieje zostanie zwrócony błąd.",
"dateAndTime": "Data i godzina",
"date": "Dowolna data może być użyta jako termin wykonania nowego zadania. Możesz wykorzystać datę w dowolnym z podanych formatów:",
"dateWeekday": "dowolny dzień tygodnia spowoduje wybranie najbliższej daty przypadającej na podany dzień tygodnia",
"dateCurrentYear": "spowoduje wybranie bieżącego roku",
"dateNth": "spowoduje wybranie {day}-stego dnia bieżącego miesiąca",
"dateTime": "Połącz dowolny format daty z \"{time}\" (lub {timePM}), aby ustawić godzinę.",
"repeats": "Powtarzające się zadania",
"repeatsDescription": "Aby ustawić zadanie jako powtarzające się w odstępie czasu, po prostu dodaj '{suffix}' do tekstu zadania. Ilość musi być liczbą, ale może zostać pominięta, aby użyć tylko typu (patrz przykłady)."
}
},
"team": {
"title": "Zespoły",
"noTeams": "Obecnie nie należysz do żadnego zespołu.",
"create": {
"title": "Utwórz nowy zespół",
"success": "Zespół został pomyślnie utworzony."
},
"edit": {
"title": "Edytuj zespół \"{team}\"",
"members": "Członkowie zespołu",
"search": "Wpisz, aby wyszukać użytkownika…",
"addUser": "Dodaj do zespołu",
"makeMember": "Uczyń członkiem",
"makeAdmin": "Uczyń administratorem",
"success": "Zespół został pomyślnie zaktualizowany.",
"userAddedSuccess": "Członek zespołu został pomyślnie dodany.",
"madeMember": "Użytkownik został pomyślnie mianowany członkiem zespołu.",
"madeAdmin": "Członek zespołu został pomyślnie mianowany administratorem.",
"delete": {
"header": "Usuń zespół",
"text1": "Czy na pewno chcesz usunąć ten zespół i wszystkich jego członków?",
"text2": "Wszyscy członkowie zespołu stracą dostęp do list i sekcji udostępnionych temu zespołowi. Tego NIE DA SIĘ COFNĄĆ!",
"success": "Zespół został pomyślnie usunięty."
},
"deleteUser": {
"header": "Usuń użytkownika z zespołu",
"text1": "Czy na pewno chcesz usunąć tego użytkownika z zespołu?",
"text2": "Utraci on dostęp do wszystkich list i sekcji, do których ma dostęp ten zespół. Tego NIE DA SIĘ COFNĄĆ!",
"success": "Użytkownik został pomyślnie usunięty z zespołu."
}
},
"attributes": {
"name": "Nazwa zespołu",
"namePlaceholder": "Tu wpisz nazwę zespołu…",
"nameRequired": "Proszę, podaj nazwę.",
"description": "Opis",
"descriptionPlaceholder": "Tu wpisz opis drużyny…",
"admin": "Administrator",
"member": "Członek"
}
},
"keyboardShortcuts": {
"title": "Skróty klawiszowe",
"general": "Ogólne",
"allPages": "Te skróty działają na wszystkich stronach.",
"currentPageOnly": "Te skróty działają tylko na bieżącej stronie.",
"toggleMenu": "Przełącz menu",
"quickSearch": "Otwórz pasek wyszukiwania/szybkiej akcji",
"then": "następnie",
"task": {
"title": "Widok zadań",
"done": "Ukończone",
"assign": "Przypisz do użytkownika",
"labels": "Dodaj etykiety do tego zadania",
"dueDate": "Zmień termin wykonania tego zadania",
"attachment": "Dodaj załącznik do tego zadania",
"related": "Zmodyfikuj zadania powiązane z tym zadaniem"
},
"list": {
"title": "Widoki listy",
"switchToListView": "Przełącz na widok listy",
"switchToGanttView": "Przełącz na widok Gantta",
"switchToKanbanView": "Przełącz na widok Kanban",
"switchToTableView": "Przełącz na widok tabeli"
}
},
"update": {
"available": "Dostępna jest aktualizacja Vikunji!",
"do": "Aktualizuj teraz"
},
"menu": {
"edit": "Edytuj",
"archive": "Archiwizuj",
"duplicate": "Duplikuj",
"delete": "Usuń",
"unarchive": "Cofnij archiwizację",
"setBackground": "Ustaw tło",
"share": "Udostępnij",
"newList": "Nowa lista"
},
"apiConfig": {
"url": "URL Vikunji",
"urlPlaceholder": "np. https://localhost:3456",
"change": "zmień",
"use": "Użyj instalacji Vikunji z {0}",
"error": "Nie można znaleźć lub użyć instalacji Vikunji z \"{domain}\". Wypróbuj inny adres URL.",
"success": "Używasz instalacji Vikunji z \"{domain}\".",
"urlRequired": "URL jest wymagany."
},
"loadingError": {
"failed": "Ładowanie nie powiodło się, {0}. Jeśli błąd będzie się powtarzał, {1}.",
"tryAgain": "spróbuj ponownie",
"contact": "skontaktuj się z nami"
},
"notification": {
"title": "Powiadomienia",
"none": "Nie masz żadnych powiadomień. Miłego dnia!",
"explainer": "Powiadomienia pojawią się tutaj, gdy będą miały miejsce akcje na sekcjach, listach lub zadaniach, które subskrybujesz."
},
"quickActions": {
"commands": "Polecenia",
"placeholder": "Wpisz polecenie lub wyszukiwaną frazę…",
"hint": "Możesz użyć {list}, aby ograniczyć wyszukiwanie do listy. Połącz {list} lub {label} (etykiety) z wyszukiwaną frazą, aby wyszukać zadanie z tymi etykietami lub na tej liście. Użyj {assignee}, aby wyszukiwać tylko zespoły.",
"tasks": "Zadania",
"lists": "Listy",
"teams": "Zespoły",
"newList": "Wpisz tytuł nowej listy…",
"newTask": "Wpisz tytuł nowego zadania…",
"newNamespace": "Wpisz tytuł nowej sekcji…",
"newTeam": "Wpisz nazwę nowego zespołu…",
"createTask": "Utwórz zadanie na bieżącej liście ({title})",
"createList": "Utwórz listę w bieżącej sekcji ({title})",
"cmds": {
"newTask": "Nowe zadanie",
"newList": "Nowa lista",
"newNamespace": "Nowa sekcja",
"newTeam": "Nowy zespół"
}
},
"date": {
"locale": "pl",
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"error": {
"error": "Błąd",
"success": "Sukces",
"0001": "Nie jesteś upoważniony, aby to zrobić.",
"1001": "Użytkownik o tej nazwie już istnieje.",
"1002": "Użytkownik z takim adresem e-mail już istnieje.",
"1004": "Nie podano nazwy użytkownika ani hasła.",
"1005": "Użytkownik nie istnieje.",
"1006": "Nie można uzyskać ID użytkownika.",
"1008": "Nie podano tokena resetowania hasła.",
"1009": "Nieprawidłowy token resetowania hasła.",
"1010": "Nieprawidłowy token potwierdzenia adresu e-mail.",
"1011": "Błędna nazwa użytkownika lub hasło.",
"1012": "Adres e-mail użytkownika nie został potwierdzony.",
"1013": "Nowe hasło jest puste.",
"1014": "Stare hasło jest puste.",
"1015": "TOTP jest już włączony dla tego użytkownika.",
"1016": "TOTP nie jest włączony dla tego użytkownika.",
"1017": "Kod autoryzacyjny uwierzytelniania dwuskładnikowego (TOTP) jest nieprawidłowy.",
"1018": "Ustawienie typu awatara użytkownika jest nieprawidłowe.",
"2001": "ID nie może być puste lub równe 0.",
"2002": "Niektóre dane żądania były nieprawidłowe.",
"3001": "Lista nie istnieje.",
"3004": "Aby wykonać tę akcję, musisz mieć uprawnienia do odczytu tej listy.",
"3005": "Tytuł listy nie może być pusty.",
"3006": "Współdzielona lista nie istnieje.",
"3007": "Lista o tym identyfikatorze już istnieje.",
"3008": "Lista jest zarchiwizowana i dlatego jest dostępna w trybie tylko do odczytu. Dotyczy to również wszystkich zadań powiązanych z tą listą.",
"4001": "Tekst zadania listy nie może być pusty.",
"4002": "Zadanie listy nie istnieje.",
"4003": "Wszystkie zadania edycji zbiorczej muszą należeć do tej samej listy.",
"4004": "Potrzebujesz co najmniej jednego zadania do edycji zbiorczej zadań.",
"4005": "Nie masz uprawnień, aby zobaczyć to zadanie.",
"4006": "Zadanie nie może zostać ustawione jako nadrzędne dla samego siebie.",
"4007": "Nie możesz utworzyć powiązanego zadania z nieprawidłowym rodzajem relacji.",
"4008": "Nie możesz utworzyć powiązanego zadania, które już istnieje.",
"4009": "Powiązane zadanie nie istnieje.",
"4010": "Nie można powiązać zadania z nim samym.",
"4011": "Załącznik do zadania nie istnieje.",
"4012": "Załącznik do zadania jest zbyt duży.",
"4013": "Parametr sortowania zadań jest nieprawidłowy.",
"4014": "Kolejność sortowania zadań jest nieprawidłowa.",
"4015": "Komentarz do zadania nie istnieje.",
"4016": "Nieprawidłowe pole zadania.",
"4017": "Nieprawidłowe porównanie filtra zadań.",
"4018": "Nieprawidłowe połączenie filtra zadań.",
"4019": "Nieprawidłowa wartość filtra zadań.",
"5001": "Sekcja nie istnieje.",
"5003": "Nie masz dostępu do określonej sekcji.",
"5006": "Nazwa sekcji nie może być pusta.",
"5009": "Aby wykonać tę akcję, musisz mieć uprawnienia do odczytu sekcji.",
"5010": "Ten zespół nie ma dostępu do tej sekcji.",
"5011": "Ten użytkownik ma już dostęp do tej sekcji.",
"5012": "Sekcja jest zarchiwizowana, dlatego może być dostępna tylko do odczytu.",
"6001": "Nazwa zespołu nie może być pusta.",
"6002": "Zespół nie istnieje.",
"6004": "Zespół ma już dostęp do tej sekcji lub listy.",
"6005": "Użytkownik jest już członkiem tego zespołu.",
"6006": "Nie można usunąć ostatniego członka zespołu.",
"6007": "Zespół nie ma dostępu do listy, aby wykonać tę akcję.",
"7002": "Użytkownik ma już dostęp do tej listy.",
"7003": "Nie masz dostępu do tej listy.",
"8001": "Ta etykieta już istnieje w tym zadaniu.",
"8002": "Etykieta nie istnieje.",
"8003": "Nie masz dostępu do tej etykiety.",
"9001": "Nieprawidłowe uprawnienia.",
"10001": "Zasobnik nie istnieje.",
"10002": "Zasobnik nie należy do tej listy.",
"10003": "Nie możesz usunąć ostatniego zasobnika z listy.",
"10004": "Nie możesz dodać zadania do tego zasobnika, ponieważ przekroczyło już limit zadań, które może pomieścić.",
"10005": "Na liście może znajdować się tylko jeden zasobnik ukończonych zadań.",
"11001": "Filtr stały nie istnieje.",
"11002": "Filtry stałe nie są dostępne dla udostępnionych linków.",
"12001": "Typ subskrypcji jest nieprawidłowy.",
"12002": "Subskrybujesz już tę jednostkę lub jej jednostkę nadrzędną.",
"13001": "Ten udostępniony link wymaga hasła do uwierzytelnienia, ale nie został podany.",
"13002": "Podane hasło udostępnionego linku było nieprawidłowe."
},
"about": {
"title": "O aplikacji",
"frontendVersion": "Wersja frontendu: {version}",
"apiVersion": "Wersja API: {version}"
}
}

View File

@ -31,9 +31,10 @@
"username": "Username",
"usernameEmail": "Username Or Email Address",
"usernamePlaceholder": "e.g. frederick",
"email": "Email address",
"email": "E-mail address",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password",
@ -44,20 +45,12 @@
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"createAccount": "Create account",
"register": "Register",
"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",
"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?",
"remember": "Stay logged in"
"logout": "Logout"
},
"settings": {
"title": "Settings",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Update Your E-Mail Address",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Sunday",
"weekStartMonday": "Monday",
"language": "Language",
"defaultList": "Default List",
"timezone": "Time Zone"
"defaultList": "Default List"
},
"totp": {
"title": "Two Factor Authentication",
@ -335,7 +327,6 @@
"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.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new lists or edit it."
},
"delete": {

File diff suppressed because it is too large Load Diff

View File

@ -31,9 +31,10 @@
"username": "Username",
"usernameEmail": "Username Or Email Address",
"usernamePlaceholder": "e.g. frederick",
"email": "Email address",
"email": "E-mail address",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password",
@ -44,20 +45,12 @@
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"createAccount": "Create account",
"register": "Register",
"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",
"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?",
"remember": "Stay logged in"
"logout": "Logout"
},
"settings": {
"title": "Settings",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Update Your E-Mail Address",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Sunday",
"weekStartMonday": "Monday",
"language": "Language",
"defaultList": "Default List",
"timezone": "Time Zone"
"defaultList": "Default List"
},
"totp": {
"title": "Two Factor Authentication",
@ -335,7 +327,6 @@
"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.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new lists or edit it."
},
"delete": {

View File

@ -31,9 +31,10 @@
"username": "Имя пользователя",
"usernameEmail": "Имя пользователя или Email",
"usernamePlaceholder": "напр. frederick",
"email": "Email address",
"email": "E-mail адрес",
"emailPlaceholder": "напр. frederic{'@'}vikunja.io",
"password": "Пароль",
"passwordRepeat": "Пароль ещё раз",
"passwordPlaceholder": "напр. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Сбросить пароль",
@ -44,20 +45,12 @@
"totpTitle": "Код двухфакторной аутентификации",
"totpPlaceholder": "напр. 123456",
"login": "Войти",
"createAccount": "Create account",
"register": "Зарегистрироваться",
"loginWith": "Войти через {provider}",
"authenticating": "Аутентификация…",
"openIdStateError": "State does not match, refusing to continue!",
"openIdGeneralError": "An error occured while authenticating against the third party.",
"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?",
"remember": "Stay logged in"
"logout": "Выйти"
},
"settings": {
"title": "Настройки",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Твой текущий пароль",
"passwordsDontMatch": "Новые пароли не совпадают.",
"passwordUpdateSuccess": "Пароль изменён.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Изменить E-mail",
"updateEmailNew": "Новый Email адрес",
"updateEmailSuccess": "E-mail успешно изменён. Для подтверждения нажми на ссылку в письме, которое мы тебе отправили.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Воскресенье",
"weekStartMonday": "Понедельник",
"language": "Язык",
"defaultList": "Список по умолчанию",
"timezone": "Time Zone"
"defaultList": "Список по умолчанию"
},
"totp": {
"title": "Двухфакторная аутентификация",
@ -105,7 +97,7 @@
"caldav": {
"title": "CalDAV",
"howTo": "Ты можешь подключить Vikunja к клиентам CalDAV, чтобы просматривать и управлять всеми задачами из разных клиентов. Введи этот URL в свой клиент:",
"more": "Подробнее о CalDAV в Vikunja"
"more": "Подробнее о caldav в Vikunja"
},
"avatar": {
"title": "Аватар",
@ -335,7 +327,6 @@
"archiveText": "Ты не сможешь изменять это пространство имён, пока не вернёшь его из архива. Это также касается всех списков в этом пространстве имён.",
"unarchiveText": "Ты сможешь создавать новые списки или изменять их.",
"success": "Пространство имён архивировано.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "Архивирование пространства имён означает, что ты не сможешь создавать в нём новые списки или изменять их."
},
"delete": {

View File

@ -31,9 +31,10 @@
"username": "Username",
"usernameEmail": "Username Or Email Address",
"usernamePlaceholder": "e.g. frederick",
"email": "Email address",
"email": "E-mail address",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password",
@ -44,20 +45,12 @@
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"createAccount": "Create account",
"register": "Register",
"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",
"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?",
"remember": "Stay logged in"
"logout": "Logout"
},
"settings": {
"title": "Settings",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Update Your E-Mail Address",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Sunday",
"weekStartMonday": "Monday",
"language": "Language",
"defaultList": "Default List",
"timezone": "Time Zone"
"defaultList": "Default List"
},
"totp": {
"title": "Two Factor Authentication",
@ -335,7 +327,6 @@
"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.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new lists or edit it."
},
"delete": {

View File

@ -31,9 +31,10 @@
"username": "Username",
"usernameEmail": "Username Or Email Address",
"usernamePlaceholder": "e.g. frederick",
"email": "Email address",
"email": "E-mail address",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password",
@ -44,20 +45,12 @@
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"createAccount": "Create account",
"register": "Register",
"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",
"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?",
"remember": "Stay logged in"
"logout": "Logout"
},
"settings": {
"title": "Settings",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Update Your E-Mail Address",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Sunday",
"weekStartMonday": "Monday",
"language": "Language",
"defaultList": "Default List",
"timezone": "Time Zone"
"defaultList": "Default List"
},
"totp": {
"title": "Two Factor Authentication",
@ -335,7 +327,6 @@
"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.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new lists or edit it."
},
"delete": {

View File

@ -31,9 +31,10 @@
"username": "Tên người dùng",
"usernameEmail": "Tên người dùng hoặc Email",
"usernamePlaceholder": "ví dụ: frederick",
"email": "Email address",
"email": "Địa chỉ Email",
"emailPlaceholder": "ví dụ: frederic{'@'}vikunja.io",
"password": "Mật khẩu",
"passwordRepeat": "Nhập lại mật khẩu",
"passwordPlaceholder": "ví dụ: •••••••••••",
"forgotPassword": "Bạn quên mật khẩu?",
"resetPassword": "Reset mật khẩu của bạn",
@ -44,20 +45,12 @@
"totpTitle": "Mã xác thực hai lớp",
"totpPlaceholder": "ví dụ: 123456",
"login": "Đăng nhập",
"createAccount": "Create account",
"register": "Đăng ký",
"loginWith": "Đăng nhập với {provider}",
"authenticating": "Đang xác thực…",
"openIdStateError": "Trạng thái không khớp, từ chối tiếp tục!",
"openIdGeneralError": "Đã xảy ra lỗi khi xác thực với bên thứ ba.",
"logout": "Đăng xuất",
"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?",
"remember": "Stay logged in"
"logout": "Đăng xuất"
},
"settings": {
"title": "Cài đặt",
@ -68,7 +61,7 @@
"currentPasswordPlaceholder": "Nhập mật khẩu hiện tại của bạn",
"passwordsDontMatch": "Mật khẩu mới và xác nhận của nó không khớp.",
"passwordUpdateSuccess": "Mật khẩu đã được cập nhật thành công.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Cập nhật địa chỉ e-mail của bạn",
"updateEmailNew": "Địa chỉ email mới",
"updateEmailSuccess": "Địa chỉ email của bạn đã được cập nhật thành công. Chúng tôi đã gửi cho bạn một liên kết để xác nhận nó.",
"general": {
@ -85,8 +78,7 @@
"weekStartSunday": "Chủ nhật",
"weekStartMonday": "Thứ hai",
"language": "Ngôn ngữ",
"defaultList": "Danh sách mặc định",
"timezone": "Time Zone"
"defaultList": "Danh sách mặc định"
},
"totp": {
"title": "Xác thực hai lớp",
@ -103,7 +95,7 @@
"disableSuccess": "Xác thực hai lớp đã bị vô hiệu hóa thành công."
},
"caldav": {
"title": "Giao thức CalDAV",
"title": "Giao thức Caldav",
"howTo": "Bạn có thể kết nối Vikunja tới các máy khách CalDAV để xem và quản lý tất cả các công việc từ nhiều máy khách khác nhau. Nhập URL này vào ứng dụng khách của bạn:",
"more": "Tìm hiểu thêm về CalDAV"
},
@ -335,7 +327,6 @@
"archiveText": "Bạn sẽ không thể chỉnh sửa góc làm việc này hoặc tạo danh sách mới cho đến khi bạn bỏ lưu trữ nó. Điều này cũng sẽ lưu trữ tất cả các danh sách trong góc làm việc này.",
"unarchiveText": "Bạn có thể tạo danh sách mới hoặc chỉnh sửa nó.",
"success": "Góc làm việc đã lưu trữ thành công.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "Nếu một góc làm việc được lưu trữ, bạn không thể tạo thêm danh sách hoặc chỉnh sửa nó."
},
"delete": {

View File

@ -16,8 +16,6 @@ import {
faCocktail,
faCoffee,
faCog,
faEye,
faEyeSlash,
faEllipsisH,
faEllipsisV,
faExclamation,
@ -89,8 +87,6 @@ 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)

View File

@ -18,7 +18,7 @@ declare global {
}
}
import {formatDate, formatDateShort, formatDateLong, formatDateSince, formatISO} from '@/helpers/time/formatDate'
import {formatDate, formatDateShort, formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
// @ts-ignore
import {VERSION} from './version.json'
@ -52,7 +52,6 @@ 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'
@ -85,7 +84,6 @@ app.mixin({
format: formatDate,
formatDate: formatDateLong,
formatDateShort: formatDateShort,
formatISO,
getNamespaceTitle,
getListTitle,
setTitle,

View File

@ -7,6 +7,7 @@ export default class BackgroundImageModel extends AbstractModel {
url: '',
thumb: '',
info: {},
blurHash: '',
}
}
}

View File

@ -20,7 +20,7 @@ export default class ListModel extends AbstractModel {
this.owner = new UserModel(this.owner)
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
if (typeof this.subscription !== 'undefined' && this.subscription !== null) {
this.subscription = new SubscriptionModel(this.subscription)
}
@ -44,6 +44,7 @@ export default class ListModel extends AbstractModel {
isFavorite: false,
subscription: null,
position: 0,
backgroundBlurHash: '',
created: null,
updated: null,

View File

@ -10,6 +10,9 @@ 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)

View File

@ -11,7 +11,6 @@ export default class UserSettingsModel extends AbstractModel {
overdueTasksRemindersEnabled: true,
defaultListId: undefined,
weekStart: 0,
timezone: '',
}
}
}

View File

@ -1,4 +1,4 @@
import {beforeEach, afterEach, describe, it, expect, vi} from 'vitest'
import {describe, it, expect} from 'vitest'
import {parseTaskText} from './parseTaskText'
import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate'
@ -6,14 +6,6 @@ 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')
})
@ -40,7 +32,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)
@ -219,36 +211,17 @@ 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 time = new Date(2022, 0, 15)
vi.setSystemTime(time)
const result = parseTaskText(`Lorem Ipsum ${time.getDate() - 1}th`)
const date = new Date()
date.setDate(date.getDate() - 1)
const result = parseTaskText(`Lorem Ipsum ${date.getDate()}nd`)
expect(result.text).toBe('Lorem Ipsum')
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)
expect(result.date.getDate()).toBe(date.getDate())
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())
const nextMonthWithDate = result.date.getDate() === 31
? (date.getMonth() + 2) % 12
: (date.getMonth() + 1) % 12
expect(result.date.getMonth()).toBe(nextMonthWithDate)
})
it('should recognize dates of the month in the future', () => {
const nextDay = new Date(+new Date() + 60 * 60 * 24 * 1000)
@ -269,12 +242,6 @@ 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', () => {

View File

@ -2,8 +2,6 @@ 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'
@ -15,8 +13,9 @@ import DataExportDownload from '../views/user/DataExportDownload.vue'
// Tasks
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange.vue'
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth.vue'
import ListNamespaces from '../views/namespaces/ListNamespaces.vue'
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal.vue'
import TaskDetailView from '../views/tasks/TaskDetailView.vue'
import ListNamespaces from '../views/namespaces/ListNamespaces.vue'
// Team Handling
import ListTeamsComponent from '../views/teams/ListTeams.vue'
// Label Handling
@ -26,11 +25,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 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'
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'
// List Settings
import ListSettingEdit from '../views/list/settings/edit.vue'
import ListSettingBackground from '../views/list/settings/background.vue'
@ -81,7 +80,7 @@ const router = createRouter({
// Scroll to anchor should still work
if (to.hash) {
return {el: to.hash}
return {el: document.getElementById(to.hash.slice(1))}
}
// Otherwise just scroll to the top
@ -133,7 +132,7 @@ const router = createRouter({
name: 'user.register',
component: RegisterComponent,
meta: {
title: 'user.auth.createAccount',
title: 'user.auth.register',
},
},
{
@ -202,170 +201,320 @@ const router = createRouter({
{
path: '/namespaces/new',
name: 'namespace.create',
component: NewNamespaceComponent,
meta: {
showAsModal: true,
components: {
popup: NewNamespaceComponent,
},
},
{
path: '/namespaces/:id/list',
name: 'list.create',
components: {
popup: NewListComponent,
},
},
{
path: '/namespaces/:id/settings/edit',
name: 'namespace.settings.edit',
component: NamespaceSettingEdit,
meta: {
showAsModal: true,
components: {
popup: NamespaceSettingEdit,
},
},
{
path: '/namespaces/:namespaceId/settings/share',
path: '/namespaces/:id/settings/share',
name: 'namespace.settings.share',
component: NamespaceSettingShare,
meta: {
showAsModal: true,
components: {
popup: NamespaceSettingShare,
},
},
{
path: '/namespaces/:id/settings/archive',
name: 'namespace.settings.archive',
component: NamespaceSettingArchive,
meta: {
showAsModal: true,
components: {
popup: NamespaceSettingArchive,
},
},
{
path: '/namespaces/:id/settings/delete',
name: 'namespace.settings.delete',
component: NamespaceSettingDelete,
meta: {
showAsModal: true,
components: {
popup: NamespaceSettingDelete,
},
},
{
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',
component: ListSettingEdit,
meta: {
showAsModal: true,
components: {
popup: ListSettingEdit,
},
},
{
path: '/lists/:listId/settings/background',
name: 'list.settings.background',
component: ListSettingBackground,
meta: {
showAsModal: true,
components: {
popup: ListSettingBackground,
},
},
{
path: '/lists/:listId/settings/duplicate',
name: 'list.settings.duplicate',
component: ListSettingDuplicate,
meta: {
showAsModal: true,
components: {
popup: ListSettingDuplicate,
},
},
{
path: '/lists/:listId/settings/share',
name: 'list.settings.share',
component: ListSettingShare,
meta: {
showAsModal: true,
components: {
popup: ListSettingShare,
},
},
{
path: '/lists/:listId/settings/delete',
name: 'list.settings.delete',
component: ListSettingDelete,
meta: {
showAsModal: true,
components: {
popup: ListSettingDelete,
},
},
{
path: '/lists/:listId/settings/archive',
name: 'list.settings.archive',
component: ListSettingArchive,
meta: {
showAsModal: true,
components: {
popup: ListSettingArchive,
},
},
{
path: '/lists/:listId/settings/edit',
name: 'filter.settings.edit',
component: FilterEdit,
meta: {
showAsModal: true,
components: {
popup: FilterEdit,
},
},
{
path: '/lists/:listId/settings/delete',
name: 'filter.settings.delete',
component: FilterDelete,
meta: {
showAsModal: true,
components: {
popup: FilterDelete,
},
},
{
path: '/lists/:listId',
name: 'list.index',
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) }),
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,
},
],
},
],
},
{
path: '/teams',
@ -375,9 +524,8 @@ const router = createRouter({
{
path: '/teams/new',
name: 'teams.create',
component: NewTeamComponent,
meta: {
showAsModal: true,
components: {
popup: NewTeamComponent,
},
},
{
@ -393,9 +541,8 @@ const router = createRouter({
{
path: '/labels/new',
name: 'labels.create',
component: NewLabelComponent,
meta: {
showAsModal: true,
components: {
popup: NewLabelComponent,
},
},
{
@ -411,9 +558,8 @@ const router = createRouter({
{
path: '/filters/new',
name: 'filters.create',
component: FilterNew,
meta: {
showAsModal: true,
components: {
popup: FilterNew,
},
},
{
@ -429,7 +575,11 @@ const router = createRouter({
],
})
export function getAuthForRoute(route: RouteLocation) {
router.beforeEach((to) => {
return checkAuth(to)
})
function checkAuth(route: RouteLocation) {
const authUser = store.getters['auth/authUser']
const authLinkShare = store.getters['auth/authLinkShare']

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