Compare commits

..

70 Commits

Author SHA1 Message Date
renovate 1138285fd9 fix(deps): update dependency @intlify/unplugin-vue-i18n to v1.6.0
continuous-integration/drone/pr Build is failing Details
2023-12-13 04:32:00 +00:00
konrad 923fc4eaa0 fix(editor): keep editor open when emptying content from the outside (#3852)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3852
2023-12-11 23:13:30 +00:00
kolaente ea7dab68ae
fix(editor): add workaround for checklist tiptap bug
continuous-integration/drone/pr Build is passing Details
2023-12-11 23:58:46 +01:00
kolaente 8b9e5e54af
fix(test): use correct file input
continuous-integration/drone/pr Build is failing Details
2023-12-11 23:23:25 +01:00
kolaente a4a2b95dc7
chore: cleanup
continuous-integration/drone/pr Build is failing Details
2023-12-11 22:37:28 +01:00
kolaente 9fdb6a8d24
feat(task): add more tests
continuous-integration/drone/pr Build was killed Details
2023-12-11 22:35:32 +01:00
kolaente fc6b707405
fix(task): use empty description helper everywhere 2023-12-11 22:35:09 +01:00
kolaente 9efe860f26
fix(editor): keep editor open when emptying content from the outside 2023-12-11 21:58:39 +01:00
kolaente af13d68c48
fix(editor): show editor if there is no content initially 2023-12-11 21:55:47 +01:00
kolaente 3cb1e7dede
chore: debug 2023-12-11 21:01:38 +01:00
renovate e27d88785e fix(deps): update dependency vue to v3.3.10 (#3843)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3843
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-06 13:01:56 +00:00
renovate b1fc5dbd97 chore(deps): update dev-dependencies (major) (#3827)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3827
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-06 12:42:11 +00:00
renovate 1786dee042 fix(deps): update dependency @github/hotkey to v2.3.1 (#3845)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3845
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-06 12:19:03 +00:00
renovate 8491caf419 fix(deps): update vueuse to v10.7.0 (#3844)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3844
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-06 12:16:48 +00:00
renovate 3b15293b47 chore(deps): update dev-dependencies (#3842)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3842
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-06 12:07:43 +00:00
renovate e65f13eaa3 fix(deps): update dependency sortablejs to v1.15.1 (#3841)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3841
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-06 12:04:24 +00:00
renovate 25561f3229 fix(deps): update dependency vue-i18n to v9.8.0 (#3833)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3833
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-06 12:02:39 +00:00
renovate 97d149c2f5 fix(deps): update sentry-javascript monorepo to v7.85.0 (#3831)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3831
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-06 11:59:40 +00:00
kolaente c8809d899a
fix(kanban): check if doneBucketId is set
continuous-integration/drone/push Build is passing Details
2023-12-01 15:10:48 +01:00
renovate 203041ae36 fix(deps): update dependency vue to v3.3.9 (#3837)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3837
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-01 11:36:42 +00:00
renovate 11d11012e7 chore(deps): update dependency node (#3834)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3834
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-01 11:07:14 +00:00
renovate 30046c7ff5 chore(deps): update dev-dependencies (#3835)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3835
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-01 10:53:44 +00:00
renovate 668ff753b3 fix(deps): update font awesome to v6.5.1 (#3839)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3839
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-01 10:50:30 +00:00
renovate 0b68ab93e1 fix(deps): update tiptap to v2.1.13 (#3840)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3840
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-12-01 10:50:22 +00:00
renovate af22d2e88a chore(deps): update pnpm to v8.11.0
continuous-integration/drone/push Build is passing Details
2023-12-01 10:31:40 +00:00
kolaente 611e9feb6d
chore(deps): update sub-dependencies
continuous-integration/drone/push Build is passing Details
2023-11-28 22:50:15 +01:00
kolaente e770496524
fix(editor): don't check parent checkbox when child label was clicked
continuous-integration/drone/push Build is passing Details
2023-11-27 13:11:24 +01:00
kolaente 1cbb93ea9b
fix(tasks): make sure tasks are fully clickable
continuous-integration/drone/push Build is passing Details
Resolves #3838
2023-11-27 12:47:26 +01:00
Frederick [Bot] f7c06e53b7 [skip ci] Updated translations via Crowdin 2023-11-27 00:25:06 +00:00
Frederick [Bot] 240906f236 [skip ci] Updated translations via Crowdin 2023-11-24 00:24:04 +00:00
renovate 282ec3164b chore(deps): update dev-dependencies (#3829)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3829
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-22 10:20:23 +00:00
Frederick [Bot] a994264234 [skip ci] Updated translations via Crowdin 2023-11-22 00:25:02 +00:00
kolaente 4868ac824e
feat(i18n): add Slovene language for selection in the ui
continuous-integration/drone/push Build is passing Details
2023-11-21 22:14:15 +01:00
kolaente 0c58ea1ade
fix(editor): don't crash when the component isn't completely mounted
continuous-integration/drone/push Build is passing Details
2023-11-21 13:25:55 +01:00
kolaente f45303c2e3
fix(editor): image paste handling 2023-11-21 13:23:05 +01:00
kolaente c3e53970de
chore(deps): update lockfile
continuous-integration/drone/push Build is passing Details
2023-11-21 13:03:06 +01:00
Frederick [Bot] 0795c0e448 [skip ci] Updated translations via Crowdin 2023-11-21 00:24:01 +00:00
renovate cfd46dc39b fix(deps): update vueuse to v10.6.1 (#3822)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3822
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-20 20:40:34 +00:00
kolaente debae2326e
fix(editor): don't create empty "blob" files when pasting images
continuous-integration/drone/push Build is passing Details
2023-11-20 12:35:19 +01:00
renovate 23d670525d chore(deps): update dessant/repo-lockdown action to v4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-11-20 05:13:05 +00:00
kolaente 2967019cd9
feat(editor): mark a checkbox item as done when clicking on its text
continuous-integration/drone/push Build is passing Details
2023-11-18 18:01:09 +01:00
kolaente d3497c96d7
fix(editor): correctly resolve images in descriptions
continuous-integration/drone/push Build is failing Details
Resolves #3808
2023-11-18 17:17:14 +01:00
kolaente bd83294ac0
fix(editor): alignment and focus states
continuous-integration/drone/push Build is failing Details
2023-11-18 17:03:47 +01:00
kolaente 6c4f1e1cbf
fix(editor): make initial editor mode (preview/edit) work
continuous-integration/drone/push Build is failing Details
2023-11-18 16:54:29 +01:00
kolaente fa269f155a
chore(filter): remove debug log
continuous-integration/drone/push Build is failing Details
2023-11-18 16:44:51 +01:00
kolaente 602d15985b
fix(filter): don't immediately re-trigger prepareFilter
continuous-integration/drone/push Build is failing Details
2023-11-18 16:40:20 +01:00
renovate cc3c1a9429 chore(deps): update dev-dependencies (#3828)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3828
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-18 14:21:37 +00:00
renovate cfd49864e1 fix(deps): update dependency axios to v1.6.2 (#3820)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3820
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-18 14:21:02 +00:00
renovate 6711a08de9 fix(deps): update sentry-javascript monorepo to v7.80.1 (#3819)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3819
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:50:00 +00:00
renovate 7fe33c6662 fix(deps): update dependency @types/sortablejs to v1.15.5 (#3818)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3818
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:49:51 +00:00
renovate e61b215dc1 fix(deps): update dependency ufo to v1.3.2 (#3824)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3824
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:03:25 +00:00
renovate 3b5cb1ade3 fix(deps): update dependency vue-i18n to v9.7.0 (#3825)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3825
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:01:43 +00:00
renovate 89e28cbdf2 chore(deps): update dev-dependencies (#3826)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3826
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 21:59:59 +00:00
renovate e9e836f068 chore(deps): update pnpm to v8.10.5
continuous-integration/drone/push Build is passing Details
2023-11-17 21:40:42 +00:00
kolaente aa5e11915e
fix(filter): don't prevent entering date math strings
continuous-integration/drone/push Build is passing Details
Resolves https://community.vikunja.io/t/filter-setting-s/1791/2
2023-11-17 19:38:55 +01:00
kolaente 7f279c98e1
fix(tasks): don't use the filter for upcoming when one is set for the home page
continuous-integration/drone/push Build is passing Details
Resolves https://github.com/go-vikunja/frontend/issues/132
2023-11-17 19:08:08 +01:00
kolaente 3c1861eb6a
fix(settings): move overdue remindeer time below
continuous-integration/drone/push Build is passing Details
2023-11-17 19:03:58 +01:00
kolaente 75262b716f
fix(kanban): opening a task from the kanban board and then reloading the page should not crash everything when then navigating back
continuous-integration/drone/push Build is passing Details
Before this fix, the following would not work:

1. Open the kanban view of a project
2. Click on a task to open it in a modal
3. Reload the page
4. Using your browser's back button, navigate back

Instead of showing the kanban board with the task modal closed, it would
navigate to `/projects/0/kanban` and crash.
2023-11-15 23:43:39 +01:00
ThatHurleyGuy 7e623d919e fix(filters): infinite loop when creating filters with dates (#3061)
continuous-integration/drone/push Build is passing Details
Rather than putting in a truncated version of the date/time with `startDate.getDate`, use the iso formatted version which includes the timezone data. I have no idea if this has ramifications elsewhere in the app, but it solves the problems I was seeing.

Co-authored-by: Sean Hurley <sean.hurley6@gmail.com>
Reviewed-on: #3061
Reviewed-by: konrad <k@knt.li>
Co-authored-by: ThatHurleyGuy <sean@hurley.io>
Co-committed-by: ThatHurleyGuy <sean@hurley.io>
2023-11-15 12:10:18 +00:00
kolaente 3f42ce2b34
fix(filter): make other filters are not available for project selection
continuous-integration/drone/push Build is passing Details
2023-11-15 12:47:19 +01:00
renovate 8b8da40265 chore(deps): update dev-dependencies (#3821)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3821
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-15 11:23:57 +00:00
Frederick [Bot] 0f23cc2162 [skip ci] Updated translations via Crowdin 2023-11-14 00:13:32 +00:00
Frederick [Bot] adf80d9184 [skip ci] Updated translations via Crowdin 2023-11-11 00:13:51 +00:00
Frederick [Bot] e3dfcafc29 [skip ci] Updated translations via Crowdin 2023-11-10 00:13:52 +00:00
Frederick [Bot] a9df58109f [skip ci] Updated translations via Crowdin 2023-11-09 00:14:14 +00:00
kolaente 59a7360608
feat(migration): proper wording for async migration
continuous-integration/drone/push Build is passing Details
2023-11-09 00:14:37 +01:00
renovate 29e128c64c chore(deps): update dev-dependencies (#3813)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3813
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 19:24:57 +00:00
renovate cec50d912c fix(deps): update dependency vue to v3.3.8 (#3814)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3814
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 19:24:39 +00:00
renovate 53564ec46c fix(deps): update dependency @types/lodash.clonedeep to v4.5.9 (#3817)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3817
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 17:07:48 +00:00
renovate e9cd7aac69 fix(deps): update dependency @intlify/unplugin-vue-i18n to v1.5.0 (#3812)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3812
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 17:06:56 +00:00
56 changed files with 4213 additions and 2633 deletions

View File

@ -42,7 +42,7 @@ steps:
# - .cache
- name: dependencies
image: node:20.9-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -55,7 +55,7 @@ steps:
# - restore-cache
- name: lint
image: node:20.9-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -66,7 +66,7 @@ steps:
- dependencies
- name: build-prod
image: node:20.9-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -77,7 +77,7 @@ steps:
- dependencies
- name: test-unit
image: node:20.9-alpine
image: node:20.10-alpine
pull: always
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
@ -87,7 +87,7 @@ steps:
- name: typecheck
failure: ignore
image: node:20.9-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -202,7 +202,7 @@ steps:
# - .cache
- name: build
image: node:20.9-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -285,7 +285,7 @@ steps:
# - .cache
- name: build
image: node:20.9-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -532,6 +532,6 @@ steps:
src/i18n/lang/en.json: en.json
---
kind: signature
hmac: dab902060979f246df77641c995c843ea39f86dba2de9003da7e593ce6f6f08a
hmac: 3380c4283256eea047e6228817161991d23457d09abe9d99f06e018b1eb047f4
...

View File

@ -12,7 +12,7 @@ jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/repo-lockdown@v3
- uses: dessant/repo-lockdown@v4
with:
pr-comment: 'Hi! Thank you for your contribution.

2
.nvmrc
View File

@ -1 +1 @@
20.9.0
20.10.0

View File

@ -3,7 +3,7 @@
# │─││ │││ │ │
# ┘─┘┘─┘┘┘─┘┘─┘
FROM --platform=$BUILDPLATFORM node:20.9-alpine AS builder
FROM --platform=$BUILDPLATFORM node:20.10-alpine AS builder
WORKDIR /build

View File

@ -5,6 +5,19 @@ import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
function createSingleTaskInBucket(count = 1, attrs = {}) {
const projects = ProjectFactory.create(1)
const buckets = BucketFactory.create(2, {
project_id: projects[0].id,
})
const tasks = TaskFactory.create(count, {
project_id: projects[0].id,
bucket_id: buckets[0].id,
...attrs,
})
return tasks[0]
}
describe('Project View Kanban', () => {
createFakeUserAndLogin()
prepareProjects()
@ -207,15 +220,7 @@ describe('Project View Kanban', () => {
})
it('Should remove a task from the board when deleting it', () => {
const projects = ProjectFactory.create(1)
const buckets = BucketFactory.create(2, {
project_id: projects[0].id,
})
const tasks = TaskFactory.create(5, {
project_id: 1,
bucket_id: buckets[0].id,
})
const task = tasks[0]
const task = createSingleTaskInBucket(5)
cy.visit('/projects/1/kanban')
cy.get('.kanban .bucket .tasks .task')
@ -238,4 +243,43 @@ describe('Project View Kanban', () => {
cy.get('.kanban .bucket .tasks')
.should('not.contain', task.title)
})
it('Should show a task description icon if the task has a description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: 'Lorem Ipsum',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('exist')
})
it('Should not show a task description icon if the task has an empty description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: '',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('not.exist')
})
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
description: '<p></p>',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('not.exist')
})
})

View File

@ -36,7 +36,7 @@ function uploadAttachmentAndVerify(taskId: number) {
cy.get('.task-view .action-buttons .button')
.contains('Add Attachments')
.click()
cy.get('input[type=file]', {timeout: 1000})
cy.get('input[type=file]#files', {timeout: 1000})
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
cy.wait('@uploadAttachment')
@ -112,10 +112,50 @@ describe('Task', () => {
.should('contain', 'Favorites')
})
it('Should show a task description icon if the task has a description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: 'Lorem Ipsum',
})
cy.visit('/projects/1/list')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
.should('exist')
})
it('Should not show a task description icon if the task has an empty description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: '',
})
cy.visit('/projects/1/list')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
.should('not.exist')
})
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: '<p></p>',
})
cy.visit('/projects/1/list')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
.should('not.exist')
})
describe('Task Detail View', () => {
beforeEach(() => {
TaskCommentFactory.truncate()
LabelTaskFactory.truncate()
TaskAttachmentFactory.truncate()
})
it('Shows all task details', () => {
@ -213,6 +253,45 @@ describe('Task', () => {
.should('exist')
})
it('Shows an empty editor when the description of a task is empty', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: '',
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
.should('have.attr', 'data-placeholder')
cy.get('.task-view .details.content.description .tiptap button.done-edit')
.should('not.exist')
})
it('Shows a preview editor when the description of a task is not empty', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: 'Lorem Ipsum dolor sit amet',
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
.should('not.have.attr', 'data-placeholder')
cy.get('.task-view .details.content.description .tiptap button.done-edit')
.should('exist')
})
it('Shows a preview editor when the description of a task contains html', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: '<p>Lorem Ipsum dolor sit amet</p>',
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.content.description .tiptap.ProseMirror p')
.should('not.have.attr', 'data-placeholder')
cy.get('.task-view .details.content.description .tiptap button.done-edit')
.should('exist')
})
it('Can add a new comment', () => {
const tasks = TaskFactory.create(1, {
id: 1,
@ -692,7 +771,7 @@ describe('Task', () => {
.should('exist')
})
it('Can check items off a checklist', () => {
it.only('Can check items off a checklist', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: `
@ -761,7 +840,7 @@ describe('Task', () => {
.should('exist')
})
it.only('Should render an image from attachment', async () => {
it('Should render an image from attachment', async () => {
TaskAttachmentFactory.truncate()

View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1697730408,
"narHash": "sha256-Ww//zzukdTrwTrCUkaJA/NsaLEfUfQpWZXBdXBYfhak=",
"lastModified": 1701336116,
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ff0a5a776b56e0ca32d47a4a47695452ec7f7d80",
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
"type": "github"
},
"original": {

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.10.2",
"packageManager": "pnpm@8.11.0",
"keywords": [
"todo",
"productivity",
@ -45,55 +45,54 @@
"story:preview": "histoire preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/vue-fontawesome": "3.0.5",
"@github/hotkey": "2.3.0",
"@github/hotkey": "2.3.1",
"@infectoone/vue-ganttastic": "2.2.0",
"@intlify/unplugin-vue-i18n": "1.5.0",
"@intlify/unplugin-vue-i18n": "1.6.0",
"@kyvg/vue3-notification": "3.0.2",
"@sentry/tracing": "7.77.0",
"@sentry/vue": "7.77.0",
"@tiptap/core": "2.1.12",
"@tiptap/extension-blockquote": "2.1.12",
"@tiptap/extension-bold": "2.1.12",
"@tiptap/extension-bullet-list": "2.1.12",
"@tiptap/extension-code": "2.1.12",
"@tiptap/extension-code-block-lowlight": "2.1.12",
"@tiptap/extension-document": "2.1.12",
"@tiptap/extension-dropcursor": "2.1.12",
"@tiptap/extension-gapcursor": "2.1.12",
"@tiptap/extension-hard-break": "2.1.12",
"@tiptap/extension-heading": "2.1.12",
"@tiptap/extension-history": "2.1.12",
"@tiptap/extension-horizontal-rule": "2.1.12",
"@tiptap/extension-image": "2.1.12",
"@tiptap/extension-italic": "2.1.12",
"@tiptap/extension-link": "2.1.12",
"@tiptap/extension-list-item": "2.1.12",
"@tiptap/extension-ordered-list": "2.1.12",
"@tiptap/extension-paragraph": "2.1.12",
"@tiptap/extension-placeholder": "2.1.12",
"@tiptap/extension-strike": "2.1.12",
"@tiptap/extension-table": "2.1.12",
"@tiptap/extension-table-cell": "2.1.12",
"@tiptap/extension-table-header": "2.1.12",
"@tiptap/extension-table-row": "2.1.12",
"@tiptap/extension-task-item": "2.1.12",
"@tiptap/extension-task-list": "2.1.12",
"@tiptap/extension-text": "2.1.12",
"@tiptap/extension-typography": "2.1.12",
"@tiptap/extension-underline": "2.1.12",
"@tiptap/pm": "2.1.12",
"@tiptap/suggestion": "2.1.12",
"@tiptap/vue-3": "2.1.12",
"@sentry/tracing": "7.85.0",
"@sentry/vue": "7.85.0",
"@tiptap/core": "2.1.13",
"@tiptap/extension-blockquote": "2.1.13",
"@tiptap/extension-bold": "2.1.13",
"@tiptap/extension-bullet-list": "2.1.13",
"@tiptap/extension-code": "2.1.13",
"@tiptap/extension-code-block-lowlight": "2.1.13",
"@tiptap/extension-document": "2.1.13",
"@tiptap/extension-dropcursor": "2.1.13",
"@tiptap/extension-gapcursor": "2.1.13",
"@tiptap/extension-hard-break": "2.1.13",
"@tiptap/extension-heading": "2.1.13",
"@tiptap/extension-history": "2.1.13",
"@tiptap/extension-horizontal-rule": "2.1.13",
"@tiptap/extension-image": "2.1.13",
"@tiptap/extension-italic": "2.1.13",
"@tiptap/extension-link": "2.1.13",
"@tiptap/extension-list-item": "2.1.13",
"@tiptap/extension-ordered-list": "2.1.13",
"@tiptap/extension-paragraph": "2.1.13",
"@tiptap/extension-placeholder": "2.1.13",
"@tiptap/extension-strike": "2.1.13",
"@tiptap/extension-table": "2.1.13",
"@tiptap/extension-table-cell": "2.1.13",
"@tiptap/extension-table-header": "2.1.13",
"@tiptap/extension-table-row": "2.1.13",
"@tiptap/extension-task-item": "2.1.13",
"@tiptap/extension-task-list": "2.1.13",
"@tiptap/extension-text": "2.1.13",
"@tiptap/extension-typography": "2.1.13",
"@tiptap/extension-underline": "2.1.13",
"@tiptap/pm": "2.1.13",
"@tiptap/suggestion": "2.1.13",
"@tiptap/vue-3": "2.1.13",
"@types/is-touch-device": "1.0.2",
"@types/lodash.clonedeep": "4.5.8",
"@types/sortablejs": "1.15.4",
"@vueuse/core": "10.5.0",
"@vueuse/router": "10.5.0",
"axios": "1.6.0",
"@types/lodash.clonedeep": "4.5.9",
"@vueuse/core": "10.7.0",
"@vueuse/router": "10.7.0",
"axios": "1.6.2",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
@ -111,13 +110,13 @@
"pinia": "2.1.7",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.0",
"sortablejs": "1.15.1",
"tippy.js": "6.3.7",
"ufo": "1.3.1",
"vue": "3.3.7",
"ufo": "1.3.2",
"vue": "3.3.10",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.3",
"vue-i18n": "9.6.5",
"vue-i18n": "9.8.0",
"vue-router": "4.2.5",
"workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3"
@ -126,56 +125,56 @@
"@4tw/cypress-drag-drop": "2.2.5",
"@cypress/vite-dev-server": "5.0.6",
"@cypress/vue": "6.0.0",
"@faker-js/faker": "8.2.0",
"@histoire/plugin-screenshot": "0.17.0",
"@histoire/plugin-vue": "0.17.4",
"@rushstack/eslint-patch": "1.5.1",
"@faker-js/faker": "8.3.1",
"@histoire/plugin-screenshot": "0.17.6",
"@histoire/plugin-vue": "0.17.6",
"@rushstack/eslint-patch": "1.6.0",
"@tsconfig/node18": "18.2.2",
"@types/codemirror": "5.60.12",
"@types/dompurify": "3.0.4",
"@types/flexsearch": "0.7.5",
"@types/is-touch-device": "1.0.1",
"@types/lodash.debounce": "4.0.8",
"@types/codemirror": "5.60.15",
"@types/dompurify": "3.0.5",
"@types/flexsearch": "0.7.6",
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.8.10",
"@types/node": "20.10.3",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.4",
"@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.9.1",
"@vitejs/plugin-legacy": "4.1.1",
"@vitejs/plugin-vue": "4.4.0",
"@types/sortablejs": "1.15.7",
"@typescript-eslint/eslint-plugin": "6.13.2",
"@typescript-eslint/parser": "6.13.2",
"@vitejs/plugin-legacy": "5.2.0",
"@vitejs/plugin-vue": "4.5.1",
"@vue/eslint-config-typescript": "12.0.0",
"@vue/test-utils": "2.4.1",
"@vue/test-utils": "2.4.3",
"@vue/tsconfig": "0.4.0",
"autoprefixer": "10.4.16",
"browserslist": "4.22.1",
"caniuse-lite": "1.0.30001559",
"browserslist": "4.22.2",
"caniuse-lite": "1.0.30001566",
"css-has-pseudo": "6.0.0",
"csstype": "3.1.2",
"cypress": "13.4.0",
"esbuild": "0.19.5",
"eslint": "8.52.0",
"eslint-plugin-vue": "9.18.1",
"cypress": "13.6.1",
"esbuild": "0.19.8",
"eslint": "8.55.0",
"eslint-plugin-vue": "9.19.2",
"happy-dom": "12.10.3",
"histoire": "0.17.4",
"postcss": "8.4.31",
"histoire": "0.17.6",
"postcss": "8.4.32",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.0",
"postcss-preset-env": "9.3.0",
"rollup": "4.2.0",
"rollup-plugin-visualizer": "5.9.2",
"rollup": "4.6.1",
"rollup-plugin-visualizer": "5.10.0",
"sass": "1.69.5",
"start-server-and-test": "2.0.1",
"typescript": "5.2.2",
"vite": "4.5.0",
"start-server-and-test": "2.0.3",
"typescript": "5.3.2",
"vite": "5.0.6",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.16.6",
"vite-plugin-pwa": "0.17.3",
"vite-plugin-sentry": "1.3.0",
"vite-svg-loader": "4.0.0",
"vitest": "0.34.6",
"vue-tsc": "1.8.22",
"wait-on": "7.1.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.0.1",
"vue-tsc": "1.8.25",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
},
"pnpm": {

File diff suppressed because it is too large Load Diff

View File

@ -75,6 +75,7 @@ import {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import Popup from '@/components/misc/popup.vue'
import {DATE_RANGES} from '@/components/date/dateRanges'
@ -120,9 +121,9 @@ watch(
to.value = newValue.dateTo
// Only set the date back to flatpickr when it's an actual date.
// Otherwise flatpickr runs in an endless loop and slows down the browser.
const dateFrom = new Date(from.value)
const dateTo = new Date(to.value)
if (dateTo.getTime() === dateTo.getTime() && dateFrom.getTime() === dateFrom.getTime()) {
const dateFrom = parseDateOrString(from.value, false)
const dateTo = parseDateOrString(to.value, false)
if (dateFrom instanceof Date && dateTo instanceof Date) {
flatpickrRange.value = `${from.value} to ${to.value}`
}
},

View File

@ -39,7 +39,7 @@
</router-view>
<modal
:enabled="Boolean(currentModal)"
:enabled="typeof currentModal !== 'undefined'"
@close="closeModal()"
variant="scrolling"
class="task-detail-view-modal"

View File

@ -20,11 +20,20 @@ import type {IProject} from '@/modelTypes/IProject'
import ProjectService from '@/services/project'
import {includesById} from '@/helpers/utils'
type ProjectFilterFunc = (p: IProject) => boolean
const props = defineProps({
modelValue: {
type: Array as PropType<IProject[]>,
default: () => [],
},
projectFilter: {
type: Function as PropType<ProjectFilterFunc>,
default: () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return (_: IProject) => true
},
},
})
const emit = defineEmits<{
(e: 'update:modelValue', value: IProject[]): void
@ -58,6 +67,8 @@ async function findProjects(query: string) {
const response = await projectService.getAll({}, {s: query}) as IProject[]
// Filter selected items from the results
foundProjects.value = response.filter(({id}) => !includesById(projects.value, id))
foundProjects.value = response
.filter(({id}) => !includesById(projects.value, id))
.filter(props.projectFilter)
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div class="tiptap">
<div class="tiptap" ref="tiptapInstanceRef">
<EditorToolbar
v-if="editor && isEditing"
:editor="editor"
@ -62,7 +62,7 @@
<editor-content
class="tiptap__editor"
:class="{'tiptap__editor-is-empty': isEmpty, 'tiptap__editor-is-edit-enabled': isEditing}"
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
:editor="editor"
/>
@ -117,8 +117,7 @@
</template>
<script setup lang="ts">
import {ref, watch, onBeforeUnmount, nextTick, onMounted, computed} from 'vue'
import {refDebounced} from '@vueuse/core'
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import EditorToolbar from './EditorToolbar.vue'
@ -175,6 +174,8 @@ import {mergeAttributes} from '@tiptap/core'
import {createRandomID} from '@/helpers/randomId'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
const {t} = useI18n()
const CustomTableCell = TableCell.extend({
@ -199,12 +200,33 @@ const CustomTableCell = TableCell.extend({
})
type CacheKey = `${ITask['id']}-${IAttachment['id']}`
const loadedAttachments = ref<{ [key: CacheKey]: string }>({})
const loadedAttachments = ref<{
[key: CacheKey]: string
}>({})
const CustomImage = Image.extend({
addAttributes() {
return {
src: {
default: null,
},
alt: {
default: null,
},
title: {
default: null,
},
id: {
default: null,
},
'data-src': {
default: null,
},
}
},
renderHTML({HTMLAttributes}) {
if (HTMLAttributes.src?.startsWith(window.API_URL)) {
if (HTMLAttributes.src?.startsWith(window.API_URL) || HTMLAttributes['data-src']?.startsWith(window.API_URL)) {
const imageUrl = HTMLAttributes['data-src'] ?? HTMLAttributes.src
const id = 'tiptap-image-' + createRandomID()
nextTick(async () => {
@ -213,7 +235,7 @@ const CustomImage = Image.extend({
if (!img) return
// The url is something like /tasks/<id>/attachments/<id>
const parts = img.dataset?.src.slice(window.API_URL.length + 1).split('/')
const parts = imageUrl.slice(window.API_URL.length + 1).split('/')
const taskId = Number(parts[1])
const attachmentId = Number(parts[3])
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
@ -223,15 +245,14 @@ const CustomImage = Image.extend({
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
const attachmentService = new AttachmentService()
const url = await attachmentService.getBlobUrl(attachment)
loadedAttachments.value[cacheKey] = url
loadedAttachments.value[cacheKey] = await attachmentService.getBlobUrl(attachment)
}
img.src = loadedAttachments.value[cacheKey]
})
return ['img', mergeAttributes(this.options.HTMLAttributes, {
'data-src': HTMLAttributes.src,
'data-src': imageUrl,
src: '#',
alt: HTMLAttributes.alt,
title: HTMLAttributes.title,
@ -253,7 +274,6 @@ const {
showSave = false,
placeholder = '',
editShortcut = '',
initialMode = 'edit',
} = defineProps<{
modelValue: string,
uploadCallback?: UploadCallback,
@ -262,14 +282,11 @@ const {
showSave?: boolean,
placeholder?: string,
editShortcut?: string,
initialMode?: Mode,
}>()
const emit = defineEmits(['update:modelValue', 'save'])
const inputHTML = ref('')
const isEmpty = computed(() => isEditorContentEmpty(inputHTML.value))
const internalMode = ref<Mode>(initialMode)
const internalMode = ref<Mode>('edit')
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const editor = useEditor({
@ -341,14 +358,28 @@ const editor = useEditor({
TaskItem.configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (isEditEnabled) {
node.attrs.checked = checked
inputHTML.value = editor.value?.getHTML()
bubbleSave()
return true
if (!isEditEnabled) {
return false
}
return false
// The following is a workaround for this bug:
// https://github.com/ueberdosis/tiptap/issues/4521
// https://github.com/ueberdosis/tiptap/issues/3676
editor.value!.state.doc.descendants((subnode, pos) => {
if (node.eq(subnode)) {
const {tr} = editor.value!.state
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
checked,
})
editor.value!.view.dispatch(tr)
bubbleSave()
}
})
return true
},
}),
@ -358,52 +389,55 @@ const editor = useEditor({
BubbleMenu,
],
onUpdate: () => {
inputHTML.value = editor.value!.getHTML()
bubbleNow()
},
})
watch(
() => modelValue,
value => {
inputHTML.value = value
if (!editor?.value) return
if (editor.value.getHTML() === value) {
return
}
editor.value.commands.setContent(value, false)
},
)
const debouncedInputHTML = refDebounced(inputHTML, 1000)
watch(debouncedInputHTML, () => bubbleNow())
function bubbleNow() {
emit('update:modelValue', inputHTML.value)
}
function bubbleSave() {
bubbleNow()
emit('save', inputHTML.value)
if (isEditing.value) {
internalMode.value = 'preview'
}
}
function setEdit() {
internalMode.value = 'edit'
editor.value?.commands.focus()
}
watch(
() => isEditing.value,
() => {
editor.value?.setEditable(isEditing.value)
},
{immediate: true},
)
watch(
() => modelValue,
value => {
if (!editor?.value) return
if (editor.value.getHTML() === value) {
return
}
setModeAndValue(value)
},
{immediate: true},
)
function bubbleNow() {
if (editor.value?.getHTML() === modelValue) {
return
}
emit('update:modelValue', editor.value?.getHTML())
}
function bubbleSave() {
bubbleNow()
emit('save', editor.value?.getHTML())
if (isEditing.value) {
internalMode.value = 'preview'
}
}
function setEdit(focus: boolean = true) {
internalMode.value = 'edit'
if (focus) {
editor.value?.commands.focus()
}
}
onBeforeUnmount(() => editor.value?.destroy())
const uploadInputRef = ref<HTMLInputElement | null>(null)
@ -473,28 +507,45 @@ function setLink() {
.run()
}
onMounted(() => {
internalMode.value = initialMode
document.addEventListener('paste', handleImagePaste)
onMounted(async () => {
if (editShortcut !== '') {
document.addEventListener('keydown', setFocusToEditor)
}
await nextTick()
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.addEventListener('paste', handleImagePaste)
setModeAndValue(modelValue)
})
onBeforeUnmount(() => {
document.removeEventListener('paste', handleImagePaste)
nextTick(() => {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.removeEventListener('paste', handleImagePaste)
})
if (editShortcut !== '') {
document.removeEventListener('keydown', setFocusToEditor)
}
})
function setModeAndValue(value: string) {
internalMode.value = isEditorContentEmpty(value) ? 'edit' : 'preview'
editor.value?.commands.setContent(value, false)
}
function handleImagePaste(event) {
if (event?.clipboardData?.items?.length === 0) {
return
}
event.preventDefault()
event?.clipboardData?.items?.forEach(i => {
if (i.kind === 'file' && i.type.startsWith('image/')) {
uploadAndInsertFiles([i.getAsFile()])
}
})
const image = event.clipboardData.items[0]
if (image.kind === 'file' && image.type.startsWith('image/')) {
uploadAndInsertFiles([image.getAsFile()])
}
}
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
@ -509,29 +560,84 @@ function setFocusToEditor(event) {
}
event.preventDefault()
if (initialMode === 'preview' && isEditEnabled && !isEditing.value) {
if (!isEditing.value && isEditEnabled) {
internalMode.value = 'edit'
}
editor.value?.commands.focus()
}
function clickTasklistCheckbox(event) {
event.stopImmediatePropagation()
if (event.target.localName !== 'p') {
return
}
event.target.parentNode.parentNode.firstChild.click()
}
watch(
() => isEditing.value,
editing => {
nextTick(() => {
const checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
return
}
if (editing) {
checkboxes.forEach(check => {
if (check.children.length < 2) {
return
}
// We assume the first child contains the label element with the checkbox and the second child the actual label
// When the actual label is clicked, we forward that click to the checkbox.
check.children[1].removeEventListener('click', clickTasklistCheckbox)
})
return
}
checkboxes.forEach(check => {
if (check.children.length < 2) {
return
}
// We assume the first child contains the label element with the checkbox and the second child the actual label
// When the actual label is clicked, we forward that click to the checkbox.
check.children[1].addEventListener('click', clickTasklistCheckbox)
})
})
},
{immediate: true},
)
</script>
<style lang="scss">
.tiptap__editor {
&.tiptap__editor-is-edit-enabled {
min-height: 10rem;
.ProseMirror {
padding: .5rem;
}
&:focus-within, &:focus {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
}
ul[data-type='taskList'] li > div {
cursor: text;
}
}
transition: box-shadow $transition;
border-radius: $radius;
&:focus-within, &:focus {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
}
}
.tiptap p.is-empty::before {
.tiptap p::before {
content: attr(data-placeholder);
color: var(--grey-400);
pointer-events: none;
@ -541,7 +647,7 @@ function setFocusToEditor(event) {
// Basic editor styles
.ProseMirror {
padding: .5rem;
padding: .5rem .5rem .5rem 0;
&:focus-within, &:focus {
box-shadow: none;
@ -774,6 +880,7 @@ ul[data-type='taskList'] {
> div {
flex: 1 1 auto;
cursor: pointer;
}
}

View File

@ -164,6 +164,7 @@
v-model="entities.projects"
@select="changeMultiselectFilter('projects', 'project_id')"
@remove="changeMultiselectFilter('projects', 'project_id')"
:project-filter="p => p.id > 0"
/>
</div>
</div>
@ -176,8 +177,9 @@ export const ALPHABETICAL_SORT = 'title'
</script>
<script setup lang="ts">
import {computed, nextTick, onMounted, reactive, ref, shallowReactive, toRefs, watch} from 'vue'
import {computed, nextTick, onMounted, reactive, ref, shallowReactive, toRefs} from 'vue'
import {camelCase} from 'camel-case'
import {watchDebounced} from '@vueuse/core'
import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser'
@ -273,15 +275,16 @@ onMounted(() => {
filters.value.requireAllFilters = params.value.filter_concat === 'and'
})
watch(
// Using watchDebounced to prevent the filter re-triggering itself.
// FIXME: Only here until this whole component changes a lot with the new filter syntax.
watchDebounced(
modelValue,
(value) => {
// FIXME: filters should only be converted to snake case in
// the last moment
// FIXME: filters should only be converted to snake case in the last moment
params.value = objectToSnakeCase(value)
prepareFilters()
},
{immediate: true},
{immediate: true, debounce: 500, maxWait: 1000},
)
const sortAlphabetically = computed({
@ -310,7 +313,7 @@ function prepareFilters() {
prepareDate('end_date', 'endDate')
prepareSingleValue('priority', 'priority', 'usePriority', true)
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
prepareDate('reminders')
prepareDate('reminders', 'reminders')
prepareRelatedObjectFilter('users', 'assignees')
prepareProjectsFilter()
@ -386,13 +389,13 @@ function setDateFilter(filterName, {dateFrom, dateTo}) {
change()
}
function prepareDate(filterName, variableName) {
function prepareDate(filterName: string, variableName: 'dueDate' | 'startDate' | 'endDate' | 'reminders') {
if (typeof params.value.filter_by === 'undefined') {
return
}
let foundDateStart = false
let foundDateEnd = false
let foundDateStart: boolean | string = false
let foundDateEnd: boolean | string = false
for (const i in params.value.filter_by) {
if (params.value.filter_by[i] === filterName && params.value.filter_comparator[i] === 'greater_equals') {
foundDateStart = i
@ -411,10 +414,10 @@ function prepareDate(filterName, variableName) {
const endDate = new Date(params.value.filter_value[foundDateEnd])
filters.value[variableName] = {
dateFrom: !isNaN(startDate)
? `${startDate.getFullYear()}-${startDate.getMonth() + 1}-${startDate.getDate()}`
? `${startDate.getUTCFullYear()}-${startDate.getUTCMonth() + 1}-${startDate.getUTCDate()}`
: params.value.filter_value[foundDateStart],
dateTo: !isNaN(endDate)
? `${endDate.getFullYear()}-${endDate.getMonth() + 1}-${endDate.getDate()}`
? `${endDate.getUTCFullYear()}-${endDate.getUTCMonth() + 1}-${endDate.getUTCDate()}`
: params.value.filter_value[foundDateEnd],
}
}

View File

@ -218,13 +218,19 @@ const actions = computed(() => {
])))
})
function attachmentUpload(
file: File,
onSuccess: (url: string) => void,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onError: (error: string) => void,
) {
return uploadFile(props.taskId, file, onSuccess)
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
const uploadPromises: Promise<string>[] = []
files.forEach((file: File) => {
const promise = new Promise<string>((resolve) => {
uploadFile(props.taskId, file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
})
uploadPromises.push(promise)
})
return await Promise.all(uploadPromises)
}
const taskCommentService = shallowReactive(new TaskCommentService())
@ -299,7 +305,7 @@ async function editComment() {
if (commentEdit.comment === '') {
return
}
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
@ -368,7 +374,7 @@ async function deleteComment(commentToDelete: ITaskComment) {
}
.image.is-avatar {
border-radius: 100%;
border-radius: 100%;
}
.media-content {

View File

@ -17,6 +17,7 @@
</CustomTransition>
</h3>
<editor
class="tiptap__task-description"
:is-edit-enabled="canWrite"
:upload-callback="uploadCallback"
:placeholder="$t('task.description.placeholder')"
@ -25,7 +26,6 @@
v-model="description"
@update:model-value="saveWithDelay"
@save="save"
:initial-mode="isEditorContentEmpty(description) ? 'preview' : 'edit'"
/>
</div>
</template>
@ -38,7 +38,6 @@ import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
@ -123,3 +122,10 @@ async function uploadCallback(files: File[] | FileList): (Promise<string[]>) {
}
</script>
<style lang="scss" scoped>
.tiptap__task-description {
// The exact amount of pixels we need to make the description icon align with the buttons and the form inside the editor.
// The icon is not exactly the same length on all sides so we need to hack our way around it.
margin-left: 4px;
}
</style>

View File

@ -60,7 +60,7 @@
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span v-if="task.description" class="icon">
<span v-if="!isEditorContentEmpty(task.description)" class="icon">
<icon icon="align-left"/>
</span>
<span class="icon" v-if="task.repeatAfter.amount > 0">
@ -91,6 +91,7 @@ import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const router = useRouter()

View File

@ -3,7 +3,10 @@
<div
:class="{'is-loading': taskService.loading}"
class="task loader-container"
@click.stop.self="openTaskDetail"
@mouseup.stop.self="openTaskDetail"
@mousedown.stop.self="focusTaskLink"
ref="taskContainerRef"
tabindex="-1"
>
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
@ -20,6 +23,8 @@
<div
:class="{ 'done': task.done, 'show-project': showProject && project}"
class="tasktext"
@mouseup.stop.self="openTaskDetail"
@mousedown.stop.self="focusTaskLink"
>
<span>
<router-link
@ -88,7 +93,7 @@
<span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span class="project-task-icon" v-if="task.description">
<span class="project-task-icon" v-if="!isEditorContentEmpty(task.description)">
<icon icon="align-left"/>
</span>
<span class="project-task-icon" v-if="task.repeatAfter.amount > 0">
@ -179,6 +184,7 @@ import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import {useIntervalFn} from '@vueuse/core'
import {playPopSound} from '@/helpers/playPop'
import {useAuthStore} from '@/stores/auth'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const {
theTask,
@ -313,13 +319,24 @@ function hideDeferDueDatePopup(e) {
}
const taskLink = ref<HTMLElement | null>(null)
const taskContainerRef = ref<HTMLElement | null>(null)
function hasTextSelected() {
const isTextSelected = window.getSelection().toString()
return !(typeof isTextSelected === 'undefined' || isTextSelected === '' || isTextSelected === '\n')
}
function openTaskDetail() {
const isTextSelected = window.getSelection().toString()
if (!isTextSelected) {
if (!hasTextSelected()) {
taskLink.value.$el.click()
}
}
function focusTaskLink() {
if (!hasTextSelected()) {
taskContainerRef.value.focus()
}
}
</script>
<style lang="scss" scoped>

View File

@ -24,13 +24,18 @@ export function useRouteWithModal() {
// this is adapted 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
: {}
let routeProps = undefined
if (routePropsOption) {
if (routePropsOption === true) {
routeProps = route.params
} else {
if(typeof routePropsOption === 'function') {
routeProps = routePropsOption(route)
} else {
routeProps = routePropsOption
}
}
}
if (typeof routeProps === 'undefined') {
currentModal.value = undefined

View File

@ -1,8 +1,12 @@
export function parseDateOrString(rawValue: string | undefined, fallback: unknown) {
if (typeof rawValue === 'undefined') {
export function parseDateOrString(rawValue: string | undefined | null, fallback: unknown): (unknown | string | Date) {
if (rawValue === null || typeof rawValue === 'undefined') {
return fallback
}
if (rawValue.toLowerCase().includes('now') || rawValue.toLowerCase().includes('||')) {
return rawValue
}
const d = new Date(rawValue)
return !isNaN(+d)

View File

@ -20,6 +20,7 @@ export const SUPPORTED_LOCALES = {
'ja-JP': '日本語',
'hu-HU': 'Magyar',
'ar-SA': 'اَلْعَرَبِيَّةُ',
'sl-SI': 'Slovenščina',
} as const
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES

View File

@ -6,6 +6,7 @@
"welcomeEvening": "مساء الخير {username}!",
"lastViewed": "آخر مشاهدة",
"addToHomeScreen": "أضف هذا التطبيق إلى شاشتك الرئيسة من أجل وصول أسرع وتجربة أفضل.",
"goToOverview": "Go to overview",
"project": {
"importText": "استيراد مشاريعك ومهامك من المنصات الأخرى إلى Vikunja:",
"import": "استيراد بياناتك إلى Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "الاستيراد مرة أخرى ممكن، ولكن قد يتسبب هذا بإنشاء بيانات مكررة. هل أنت متأكد؟",
"confirm": "أنا متأكد، الرجاء البدء في عملية الترحيل الآن!",
"importUpload": "لاستيراد البيانات من {name} إلى Vikunja، انقر على الزر أدناه لاختيار الملف.",
"upload": "رفع الملف"
"upload": "رفع الملف",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "التسميات",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Dobrý večer {username}!",
"lastViewed": "Naposledy zobrazeno",
"addToHomeScreen": "Přidejte tuto aplikaci na domovskou obrazovku pro rychlejší přístup a lepší zážitek.",
"goToOverview": "Přejít na přehled",
"project": {
"importText": "Importujte své projekty a úkoly z jiných služeb do Vikunja:",
"import": "Importujte svá data do Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Import je možný, ale mohl by vytvářet duplicity. Jste si jisti?",
"confirm": "Jsem si jistý, začněte migrovat!",
"importUpload": "Chcete-li importovat data z {name} do Vikunja, klikněte na tlačítko níže pro výběr souboru.",
"upload": "Nahrát soubor"
"upload": "Nahrát soubor",
"migrationStartedWillReciveEmail": "Vikunja nyní importuje vaše seznamy/projekty, úkoly, poznámky, připomenutí a soubory z {service}. Protože to bude chvíli trvat, pošleme vám e-mail až bude hotovo. Toto okno můžete nyní zavřít.",
"migrationInProgress": "Probíhá migrace. Počkejte prosím na její dokončení."
},
"label": {
"title": "Štítky",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Godaften {username}!",
"lastViewed": "Sidst vist",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Du kan godt importere igen, men kan der kan opstå dubletter. Er du sikker?",
"confirm": "Jeg er sikker, start migreringen nu!",
"importUpload": "For at importere data fra {name} til Vikunja, skal du klikke på knappen nedenfor for at vælge en fil.",
"upload": "Upload fil"
"upload": "Upload fil",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etiketter",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zuletzt angesehen",
"addToHomeScreen": "Füge diese App deinem Startbildschirm hinzu, um schneller darauf zuzugreifen und das Erlebnis zu verbessern.",
"goToOverview": "Zur Übersicht",
"project": {
"importText": "Importiere deine Projekte und Aufgaben aus anderen Diensten in Vikunja:",
"import": "Importiere deine Daten in Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Ein erneutes Importieren ist möglich, kann aber Duplikate erzeugen. Bist du sicher?",
"confirm": "Ich bin sicher, bitte starte mit der Migration!",
"importUpload": "Um Daten von {name} in Vikunja zu importieren, klicke auf die Schaltfläche unten, um eine Datei auszuwählen.",
"upload": "Datei hochladen"
"upload": "Datei hochladen",
"migrationStartedWillReciveEmail": "Vikunja wird nun deine Listen/Projekte, Aufgaben, Notizen, Erinnerungen und Dateien von {service} importieren. Da dies eine Weile dauern wird, senden wir dir eine E-Mail, sobald der Import abgeschlossen ist. Du kannst dieses Fenster jetzt schließen.",
"migrationInProgress": "Ein Import wird gerade durchgeführt. Bitte warte, bis dieser abgeschlossen ist."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zletscht ahglueget",
"addToHomeScreen": "Füge diese App deinem Startbildschirm hinzu, um schneller darauf zuzugreifen und das Erlebnis zu verbessern.",
"goToOverview": "Zur Übersicht",
"project": {
"importText": "Importiere deine Projekte und Aufgaben aus anderen Diensten in Vikunja:",
"import": "Importiere deine Daten in Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Es erneuts Importiere isch scho mögli, aber chenti Duplikaat erstelle. Bisch der sicher?",
"confirm": "Ich bin sicher, fang mit de Migration ah!",
"importUpload": "Um Daten von {name} in Vikunja zu importieren, klicke auf die Schaltfläche unten, um eine Datei auszuwählen.",
"upload": "Datei hochladen"
"upload": "Datei hochladen",
"migrationStartedWillReciveEmail": "Vikunja wird nun deine Listen/Projekte, Aufgaben, Notizen, Erinnerungen und Dateien von {service} importieren. Da dies eine Weile dauern wird, senden wir dir eine E-Mail, sobald der Import abgeschlossen ist. Du kannst dieses Fenster jetzt schließen.",
"migrationInProgress": "Ein Import wird gerade durchgeführt. Bitte warte, bis dieser abgeschlossen ist."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "¡Buenas Tardes {username}!",
"lastViewed": "Visto por última vez",
"addToHomeScreen": "Añade esta aplicación a tu pantalla de inicio para un acceso más rápido y una experiencia mejorada.",
"goToOverview": "Go to overview",
"project": {
"importText": "Importa tus proyectos y tareas de otros servicios a Vikunja:",
"import": "Importa tus datos a Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importar de nuevo es posible, pero puede crear duplicados. ¿Estás seguro?",
"confirm": "Estoy seguro, ¡por favor empieza a migrar ahora!",
"importUpload": "Para importar datos de {name} a Vikunja, haz clic en el botón de abajo para seleccionar un archivo.",
"upload": "Subir archivo"
"upload": "Subir archivo",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etiquetas",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Bonsoir {username} !",
"lastViewed": "Dernière consultation",
"addToHomeScreen": "Ajoutez cette application à votre écran d'accueil pour un accès plus rapide et une meilleure expérience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Importer vos projets et tâches dautres services dans Vikunja :",
"import": "Importer vos données dans Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importer à nouveau est possible mais peut créer des doublons. Es-tu sûr·e ?",
"confirm": "Je suis sûr·e, commencer à migrer maintenant !",
"importUpload": "Pour importer les données de {name} dans Vikunja, cliquez sur le bouton ci-dessous pour sélectionner un fichier.",
"upload": "Téléverser le fichier"
"upload": "Téléverser le fichier",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Étiquettes",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Jó estét {username}!",
"lastViewed": "Utoljára megtekintve",
"addToHomeScreen": "Adja hozzá ezt az alkalmazást a kezdőképernyőhöz a gyorsabb hozzáférés és a jobb élmény érdekében.",
"goToOverview": "Go to overview",
"project": {
"importText": "Importálja projektjeit és feladatait más szolgáltatásokból a Vikunjába:",
"import": "Importálja adatait a Vikunjába"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Az újbóli importálás lehetséges, de előfordulhat, hogy ismétlődések keletkeznek. Biztos ebben?",
"confirm": "Biztos vagyok benne, kezdje el a migrációt most!",
"importUpload": "Ha adatokat szeretne importálni a(z) {name} webhelyről a Vikunjába, kattintson az alábbi gombra a fájl kiválasztásához.",
"upload": "Fájl feltöltése"
"upload": "Fájl feltöltése",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Címkék",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Buonasera {username}!",
"lastViewed": "Ultima visualizzazione",
"addToHomeScreen": "Aggiungi questa app alla tua schermata iniziale per un accesso più veloce e un'esperienza migliore.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Importa i tuoi dati in Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importare di nuovo è possibile, ma potrebbe creare duplicati. Sei sicuro?",
"confirm": "Sono sicuro, per favore inizia adesso la migrazione!",
"importUpload": "Per importare i dati da {name} in Vikunja, fai clic sul pulsante qui sotto per selezionare un file.",
"upload": "Carica file"
"upload": "Carica file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etichette",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "こんばんは、{username}さん",
"lastViewed": "最近の表示",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "概要に移動",
"project": {
"importText": "他のサービスからVikunjaにプロジェクトやタスクをインポートします:",
"import": "Vikunjaへのデータのインポート"
@ -149,8 +150,8 @@
"title": "APIトークン",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"createAToken": "トークンの生成",
"createToken": "トークンの生成",
"30d": "30日",
"60d": "60日",
"90d": "90日",
@ -160,7 +161,7 @@
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "このトークンは二度と表示されません。安全な場所に保管してください。",
"delete": {
"header": "Delete this token",
"header": "トークンの削除",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
@ -342,7 +343,7 @@
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTaskPlaceholder": "新しいタスク名を入力…",
"addTask": "タスクの追加",
"addAnotherTask": "他のタスクを追加",
"addBucket": "新しいバケットの作成",
@ -350,7 +351,7 @@
"deleteHeaderBucket": "バケットの削除",
"deleteBucketText1": "このバケットを削除して本当によろしいですか?",
"deleteBucketText2": "This will not delete any tasks but move them into the default bucket.",
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"deleteBucketSuccess": "バケットは正常に削除されました。",
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
"collapse": "Collapse this bucket"
@ -361,15 +362,15 @@
}
},
"webhooks": {
"title": "Webhooks",
"title": "Webhook",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"delete": "Webhookの削除",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"deleteSuccess": "Webhookは正常に削除されました。",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
@ -424,7 +425,9 @@
"alreadyMigrated2": "再びインポートすることも可能ですが、重複する可能性があります。インポートして本当によろしいですか?",
"confirm": "了解!インポート開始なのだ!",
"importUpload": "{name}からVikunjaにデータをインポートするには、以下のボタンをクリックしてファイルを選択してください。",
"upload": "ファイルのアップロード"
"upload": "ファイルのアップロード",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "ラベル",
@ -524,48 +527,48 @@
"bold": "太字",
"italic": "斜体",
"strikethrough": "打ち消し線",
"underline": "Underline",
"underline": "下線",
"code": "コード",
"codeTooltip": "Capture a code snippet.",
"quote": "引用",
"quoteTooltip": "Capture a quote.",
"bulletList": "Bullet list",
"bulletList": "順序なしリスト",
"bulletListTooltip": "Create a simple bullet list.",
"unorderedList": "Unordered list",
"orderedList": "Ordered list",
"unorderedList": "順序なしリスト",
"orderedList": "順序付きリスト",
"orderedListTooltip": "Create a list with numbering.",
"cleanBlock": "Clean Block",
"link": "リンク",
"image": "画像",
"imageTooltip": "Upload an image from your computer.",
"table": {
"title": "Table",
"insert": "Insert table",
"title": "テーブル",
"insert": "テーブルの挿入",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"deleteColumn": "列の削除",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"deleteRow": "行の削除",
"deleteTable": "テーブルの削除",
"mergeCells": "セルの統合",
"splitCell": "セルの分割",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
"fixTables": "テーブルの修正"
},
"horizontalRule": "横罫",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "Side By Side",
"guide": "説明書",
"text": "Text",
"text": "テキスト",
"textTooltip": "Just start typing with plain text.",
"taskList": "Task list",
"taskList": "タスクリスト",
"taskListTooltip": "Track tasks with a to-do list.",
"undo": "Undo",
"redo": "Redo",
"undo": "元に戻す",
"redo": "やり直す",
"placeholder": "Type some text or hit '/' to see more options…"
},
"multiselect": {
@ -794,7 +797,7 @@
"delete": "関連タスクの削除",
"deleteText1": "この関連タスクを削除して本当によろしいですか?",
"select": "Select a relation kind",
"taskRequired": "Please select a task or enter a new task title.",
"taskRequired": "タスクを選択するか、新しいタスク名を入力してください。",
"kinds": {
"subtask": "サブタスク",
"parenttask": "親タスク",
@ -841,7 +844,7 @@
"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": "You can use this multiple times.",
"label1": "To add a label, simply prefix the name of the label with {prefix}.",
"label1": "ラベルを付けるには、ラベル名の前に {prefix} を入力します。",
"label2": "Vikunja will first check if the label already exist and create it if not.",
"label3": "To use spaces, simply add a \" or ' around the label name.",
"label4": "For example: {prefix}\"Label with spaces\".",
@ -891,7 +894,7 @@
"header": "Remove a user from the team",
"text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team."
"success": "ユーザーは正常にチームから削除されました。"
},
"leave": {
"title": "Leave team",
@ -995,7 +998,7 @@
"tasks": "タスク",
"projects": "プロジェクト",
"teams": "チーム",
"labels": "Labels",
"labels": "ラベル",
"newProject": "新しいプロジェクト名を入力…",
"newTask": "新しいタスク名を入力…",
"newTeam": "新しいチーム名を入力…",
@ -1071,12 +1074,12 @@
"8002": "そのラベルは存在しません。",
"8003": "You do not have access to this label.",
"9001": "The right is invalid.",
"10001": "The bucket does not exist.",
"10001": "そのバケットは存在しません。",
"10002": "The bucket does not belong to that project.",
"10003": "You cannot remove the last bucket on a project.",
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
"10005": "There can be only one done bucket per project.",
"11001": "The saved filter does not exist.",
"11001": "その絞り込み条件は存在しません。",
"11002": "絞り込み条件はリンクの共有には使用できません。",
"12001": "The subscription entity type is invalid.",
"12002": "You are already subscribed to the entity itself or a parent entity.",

View File

@ -6,6 +6,7 @@
"welcomeEvening": " 오늘 하루는 어땠나요? {username} 님!",
"lastViewed": "최근에 본 것",
"addToHomeScreen": "더 빠른 액세스와 향상된 경험을 위해 이 앱을 홈 화면에 추가하세요.",
"goToOverview": "Go to overview",
"project": {
"importText": "다른 서비스의 프로젝트 및 작업을 Vikunja로 가져옵니다.",
"import": "데이터를 Vikunja로 가져오기"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Laatst bekeken",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "Ik weet het zeker, begin nu met migreren!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Bestand uploaden"
"upload": "Bestand uploaden",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "God Morgen {username}!",
"lastViewed": "Sist sett",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Importer dine data til Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importering på nytt er mulig, men kan skape duplikater. Er du sikker?",
"confirm": "Jeg er sikker, vennligst begynn å migrere nå!",
"importUpload": "For å importere data fra {name} til Vikunja, klikk på knappen nedenfor for å velge en fil.",
"upload": "Last opp fil"
"upload": "Last opp fil",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etiketter",

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Boa noite, {username}!",
"lastViewed": "Visto por último",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "Tenho certeza, comece a migrar agora, por favor!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Enviar arquivo"
"upload": "Enviar arquivo",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etiquetas",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Boa Tarde {username}!",
"lastViewed": "Visto recentemente",
"addToHomeScreen": "Adiciona esta aplicação ao ecrã inicial para um acesso mais rápido e uma melhor experiência.",
"goToOverview": "Ir para a vista geral",
"project": {
"importText": "Importa os teus projetos e tarefas de outros serviços para o Vikunja:",
"import": "Importar os teus dados para o Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "É possível importar novamente, mas pode criar duplicados. Tens a certeza?",
"confirm": "Tenho a certeza, por favor comece a migração agora!",
"importUpload": "Para importares dados de {name} para o Vikunja, clica no botão abaixo para selecionar um ficheiro.",
"upload": "Carregar ficheiro"
"upload": "Carregar ficheiro",
"migrationStartedWillReciveEmail": "O Vikunja irá agora importar as tuas listas/projetos, tarefas, lembretes e ficheiros a partir de {service}. Como isso vai demorar algum tempo, vamos enviar-te um e-mail após a importação estar concluída. Podes agora fechar esta janela.",
"migrationInProgress": "Uma migração está atualmente em curso. Por favor, aguarda até ela estar concluída."
},
"label": {
"title": "Etiquetas",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Добрый вечер, {username}!",
"lastViewed": "Последние просмотренные",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Импортировать проекты и задачи из других сервисов в Vikunja:",
"import": "Импорт данных в Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Повторный импорт возможен, но могут возникнуть дубликаты. Продолжить?",
"confirm": "Я уверен, давай начнём миграцию!",
"importUpload": "Чтобы импортировать данные из {name} в Vikunja, нажмите кнопку ниже для выбора файла.",
"upload": "Загрузить файл"
"upload": "Загрузить файл",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Метки",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

1105
src/i18n/lang/sl-SI.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Godkväll {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Importera din data till Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Ladda upp fil"
"upload": "Ladda upp fil",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etiketter",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Xem gần đây",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Nhập lại được luôn, nhưng có thể tạo ra các trùng lặp đấy. Bạn chắc chưa?",
"confirm": "Tôi chắc như đinh, hãy bắt đầu di chuyển thôi!",
"importUpload": "Để nhập dữ liệu từ {name} vào Vikunja, hãy nhấp vào nút bên dưới để chọn tệp.",
"upload": "Tải tệp lên"
"upload": "Tải tệp lên",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Nhãn",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "中午好,{username}",
"lastViewed": "最近查看",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "可以再次导入,但这可能会造成数据重复。您确定吗?",
"confirm": "我确定, 请立即开始迁移!",
"importUpload": "请点击下面的按钮选择一个文件,将 {name} 的数据导入到 Vikunja",
"upload": "点击上传文件"
"upload": "点击上传文件",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "标签",

View File

@ -6,6 +6,7 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
@ -424,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",

View File

@ -37,7 +37,11 @@ const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler
const ProjectList = () => import('@/views/project/ProjectList.vue')
const ProjectGantt = () => import('@/views/project/ProjectGantt.vue')
const ProjectTable = () => import('@/views/project/ProjectTable.vue')
const ProjectKanban = () => import('@/views/project/ProjectKanban.vue')
// If we load the component async, using it as a backdrop view will not work. Instead, everything explodes
// with an error from the core saying "Cannot read properties of undefined (reading 'parentNode')"
// Of course, with no clear indicator of where the problem comes from.
// const ProjectKanban = () => import('@/views/project/ProjectKanban.vue')
import ProjectKanban from '@/views/project/ProjectKanban.vue'
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
// Project Settings

View File

@ -3,7 +3,7 @@
<h1>{{ $t('migrate.titleService', {name: migrator.name}) }}</h1>
<p>{{ $t('migrate.descriptionDo') }}</p>
<template v-if="message === '' && lastMigrationDate === null">
<template v-if="message === '' && lastMigrationFinishedAt === null">
<template v-if="isMigrating === false">
<template v-if="migrator.isFileMigrator">
<p>{{ $t('migrate.importUpload', {name: migrator.name}) }}</p>
@ -46,21 +46,35 @@
<p>{{ $t('migrate.inProgress') }}</p>
</div>
</template>
<div v-else-if="lastMigrationDate">
<div v-else-if="lastMigrationStartedAt && lastMigrationFinishedAt === null">
<p>
{{ $t('migrate.alreadyMigrated1', {name: migrator.name, date: formatDateLong(lastMigrationDate)}) }}<br/>
{{ $t('migrate.migrationInProgress') }}
</p>
<x-button :to="{name: 'home'}">{{ $t('home.goToOverview') }}</x-button>
</div>
<div v-else-if="lastMigrationFinishedAt">
<p>
{{
$t('migrate.alreadyMigrated1', {name: migrator.name, date: formatDateLong(lastMigrationFinishedAt)})
}}<br/>
{{ $t('migrate.alreadyMigrated2') }}
</p>
<div class="buttons">
<x-button @click="migrate">{{ $t('migrate.confirm') }}</x-button>
<x-button :to="{name: 'home'}" variant="tertiary" class="has-text-danger">{{ $t('misc.cancel') }}</x-button>
<x-button :to="{name: 'home'}" variant="tertiary" class="has-text-danger">
{{ $t('misc.cancel') }}
</x-button>
</div>
</div>
<div v-else>
<Message class="mb-4">
<Message class="mb-4" v-if="migrator.isFileMigrator">
{{ message }}
</Message>
<x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button>
<Message class="mb-4" v-else>
{{ $t('migrate.migrationStartedWillReciveEmail', {service: migrator.name}) }}
</Message>
<x-button :to="{name: 'home'}">{{ $t('home.goToOverview') }}</x-button>
</div>
</div>
</template>
@ -82,13 +96,13 @@ import {useI18n} from 'vue-i18n'
import Logo from '@/assets/logo.svg?component'
import Message from '@/components/misc/message.vue'
import AbstractMigrationService, { type MigrationConfig } from '@/services/migrator/abstractMigration'
import AbstractMigrationService, {type MigrationConfig} from '@/services/migrator/abstractMigration'
import AbstractMigrationFileService from '@/services/migrator/abstractMigrationFile'
import {formatDateLong} from '@/helpers/time/formatDate'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {MIGRATORS} from './migrators'
import {MIGRATORS, Migrator} from './migrators'
import {useTitle} from '@/composables/useTitle'
import {useProjectStore} from '@/stores/projects'
@ -104,11 +118,12 @@ const {t} = useI18n({useScope: 'global'})
const progressDotsCount = ref(PROGRESS_DOTS_COUNT)
const authUrl = ref('')
const isMigrating = ref(false)
const lastMigrationDate = ref<Date | null>(null)
const lastMigrationFinishedAt = ref<Date | null>(null)
const lastMigrationStartedAt = ref<Date | null>(null)
const message = ref('')
const migratorAuthCode = ref('')
const migrator = computed(() => MIGRATORS[props.service])
const migrator = computed<Migrator>(() => MIGRATORS[props.service])
const migrationService = shallowReactive(new AbstractMigrationService(migrator.value.id))
const migrationFileService = shallowReactive(new AbstractMigrationFileService(migrator.value.id))
@ -130,23 +145,32 @@ async function initMigration() {
if (!migratorAuthCode.value) {
return
}
const {time} = await migrationService.getStatus()
if (time) {
lastMigrationDate.value = parseDateOrNull(time)
if (lastMigrationDate.value) {
const {startedAt, finishedAt} = await migrationService.getStatus()
if (startedAt) {
lastMigrationStartedAt.value = parseDateOrNull(startedAt)
}
if (finishedAt) {
lastMigrationFinishedAt.value = parseDateOrNull(finishedAt)
if (lastMigrationFinishedAt.value) {
return
}
}
if (lastMigrationStartedAt.value && lastMigrationFinishedAt.value === null) {
// Migration already in progress
return
}
await migrate()
}
initMigration()
const uploadInput = ref<HTMLInputElement | null>(null)
async function migrate() {
isMigrating.value = true
lastMigrationDate.value = null
lastMigrationFinishedAt.value = null
message.value = ''
let migrationConfig: MigrationConfig | File = {code: migratorAuthCode.value}

View File

@ -1,7 +1,7 @@
<template>
<ProjectWrapper
class="project-kanban"
:project-id="project.id"
:project-id="projectId"
viewName="kanban"
>
<template #header>
@ -37,7 +37,7 @@
>
<div class="bucket-header" @click="() => unCollapseBucket(bucket)">
<span
v-if="project.doneBucketId === bucket.id"
v-if="project?.doneBucketId === bucket.id"
class="icon is-small has-text-success mr-2"
v-tooltip="$t('project.kanban.doneBucketHint')"
>
@ -97,7 +97,7 @@
<dropdown-item
@click.stop="toggleDoneBucket(bucket)"
v-tooltip="$t('project.kanban.doneBucketHintExtended')"
:icon-class="{'has-text-success': bucket.id === project.doneBucketId}"
:icon-class="{'has-text-success': bucket.id === project?.doneBucketId}"
icon="check-double"
>
{{ $t('project.kanban.doneBucket') }}
@ -330,9 +330,14 @@ const bucketDraggableComponentData = computed(() => ({
{'dragging-disabled': !canWrite.value},
],
}))
const {
projectId = undefined,
} = defineProps<{
projectId: number,
}>()
const canWrite = computed(() => baseStore.currentProject?.maxRight > Rights.READ)
const project = computed(() => baseStore.currentProject)
const project = computed(() => projectId ? projectStore.projects[projectId]: null)
const buckets = computed(() => kanbanStore.buckets)
const loading = computed(() => kanbanStore.isLoading)
@ -342,10 +347,9 @@ const taskLoading = computed(() => taskStore.isLoading)
watch(
() => ({
params: params.value,
project: project.value,
projectId,
}),
({params, project}) => {
const projectId = project.id
({params}) => {
if (projectId === undefined || Number(projectId) === 0) {
return
}
@ -396,7 +400,7 @@ function updateTasks(bucketId: IBucket['id'], tasks: IBucket['tasks']) {
async function updateTaskPosition(e) {
drag.value = false
// While we could just pass the bucket index in through the function call, this would not give us the
// While we could just pass the bucket index in through the function call, this would not give us the
// new bucket id when a task has been moved between buckets, only the new bucket. Using the data-bucket-id
// of the drop target works all the time.
const bucketIndex = parseInt(e.to.dataset.bucketIndex)
@ -432,7 +436,7 @@ async function updateTaskPosition(e) {
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
newBucket.id !== oldBucket.id
) {
newTask.done = project.value.doneBucketId === newBucket.id
newTask.done = project.value?.doneBucketId === newBucket.id
}
if (
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
@ -450,7 +454,7 @@ async function updateTaskPosition(e) {
try {
await taskStore.update(newTask)
// Make sure the first and second task don't both get position 0 assigned
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
@ -480,7 +484,7 @@ async function addTaskToBucket(bucketId: IBucket['id']) {
return
}
newTaskError.value[bucketId] = false
const task = await taskStore.createNewTask({
title: newTaskText.value,
bucketId,
@ -616,10 +620,10 @@ async function toggleDefaultBucket(bucket: IBucket) {
}
async function toggleDoneBucket(bucket: IBucket) {
const doneBucketId = project.value.doneBucketId === bucket.id
const doneBucketId = project.value?.doneBucketId === bucket.id
? 0
: bucket.id
await projectStore.updateProject({
...project.value,
doneBucketId,
@ -722,7 +726,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
}
&:last-of-type {
padding-bottom: .5rem;
}
}
}
.no-move {

View File

@ -182,7 +182,7 @@ async function loadPendingTasks(from: string, to: string) {
}
}
if (authStore.settings.frontendSettings.filterIdUsedOnOverview && typeof projectStore.projects[authStore.settings.frontendSettings.filterIdUsedOnOverview] !== 'undefined') {
if (showAll.value && authStore.settings.frontendSettings.filterIdUsedOnOverview && typeof projectStore.projects[authStore.settings.frontendSettings.filterIdUsedOnOverview] !== 'undefined') {
tasks.value = await taskStore.loadTasks(params, authStore.settings.frontendSettings.filterIdUsedOnOverview)
return
}

View File

@ -6,7 +6,8 @@
'is-modal': isModal,
}"
>
<div class="task-view">
<!-- Removing everything until the task is loaded to prevent empty initialization of other components -->
<div class="task-view" v-if="visible">
<Heading
:task="task"
@update:task="Object.assign(task, $event)"
@ -605,7 +606,8 @@ watch(
}
try {
Object.assign(task.value, await taskService.get({id}))
const loaded = await taskService.get({id})
Object.assign(task.value, loaded)
attachmentStore.set(task.value.attachments)
taskColor.value = task.value.hexColor
setActiveFields()

View File

@ -24,25 +24,6 @@
</label>
<project-search v-model="filterUsedInOverview" :saved-filters-only="true"/>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
{{ $t('user.settings.general.overdueReminders') }}
</label>
</div>
<div class="field" v-if="settings.overdueTasksRemindersEnabled">
<label class="label" for="overdueTasksReminderTime">
{{ $t('user.settings.general.overdueTasksRemindersTime') }}
</label>
<div class="control">
<input
@keyup.enter="updateSettings"
class="input"
id="overdueTasksReminderTime"
type="time"
v-model="settings.overdueTasksRemindersTime"/>
</div>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="settings.emailRemindersEnabled"/>
@ -67,6 +48,25 @@
{{ $t('user.settings.general.playSoundWhenDone') }}
</label>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="settings.overdueTasksRemindersEnabled"/>
{{ $t('user.settings.general.overdueReminders') }}
</label>
</div>
<div class="field" v-if="settings.overdueTasksRemindersEnabled">
<label class="label" for="overdueTasksReminderTime">
{{ $t('user.settings.general.overdueTasksRemindersTime') }}
</label>
<div class="control">
<input
@keyup.enter="updateSettings"
class="input"
id="overdueTasksReminderTime"
type="time"
v-model="settings.overdueTasksRemindersTime"/>
</div>
</div>
<div class="field">
<label class="is-flex is-align-items-center">
<span>