Compare commits

..

70 Commits

Author SHA1 Message Date
renovate 31590236aa fix(deps): update dependency axios to v1.6.6
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-24 23:19:33 +00:00
renovate 00d48a6178 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-24 06:19:41 +00:00
renovate 5169cca8d8 fix(deps): update sentry-javascript monorepo to v7.95.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-23 19:18:41 +00:00
renovate 255a7d565c chore(deps): update pnpm to v8.14.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-23 10:20:44 +00:00
renovate 8dbaee5dfb chore(deps): update dev-dependencies to v6.19.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-23 00:19:30 +00:00
renovate 69b0b19482 fix(deps): update dependency date-fns to v3.3.1
continuous-integration/drone/push Build is passing Details
2024-01-22 10:44:49 +00:00
renovate eae89d37f1 chore(deps): update pnpm to v8.14.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-22 10:19:22 +00:00
renovate 7d19859816 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-22 00:18:43 +00:00
kolaente c7b70844c6
fix(color picker): when picking a color, the color picker should not be black afterwards
continuous-integration/drone/push Build is passing Details
2024-01-21 20:25:19 +01:00
kolaente b8c21c2ade
fix(labels): text and background combination in dark mode
continuous-integration/drone/push Build is passing Details
2024-01-21 20:20:00 +01:00
kolaente 57c99a22a0
fix(editor): use manual input prompt instead of window.prompt
continuous-integration/drone/push Build is passing Details
Resolves vikunja/desktop#184
2024-01-21 20:08:10 +01:00
kolaente 8ea97f3ffc
fix(editor): use a stable image id to prevent constant re-rendering
continuous-integration/drone/push Build is passing Details
2024-01-21 15:45:18 +01:00
kolaente 0b3604d167
fix(editor): render images without crashing
continuous-integration/drone/push Build is passing Details
2024-01-21 15:00:26 +01:00
kolaente c5ba7fcb73
fix(editor): focus the editor when clicking on the whole edit container
continuous-integration/drone/push Build is passing Details
2024-01-21 13:52:13 +01:00
kolaente 5a25685d53
fix(editor): don't bubble up changes when no changes were made
continuous-integration/drone/push Build is passing Details
Related https://community.vikunja.io/t/saving-an-empty-description-in-kanban-view-break-ui/1914/3
2024-01-21 13:44:40 +01:00
kolaente da311fce9e
fix(kanban): ensure text and icon color only depends on the card background, not on the color scheme
continuous-integration/drone/push Build is passing Details
Related https://github.com/go-vikunja/frontend/issues/135#issuecomment-1900701258
2024-01-21 00:10:05 +01:00
kolaente 0fdf1ca027
fix(notifications): read indicator size
continuous-integration/drone/push Build is passing Details
2024-01-21 00:01:04 +01:00
kolaente f8e907a8c1
fix(notifications): always left-align notification text
continuous-integration/drone/push Build is passing Details
2024-01-20 23:59:57 +01:00
kolaente af7ca8ad8f
fix(project): always use the appropriate color for task estimate during deletion dialoge
continuous-integration/drone/push Build is passing Details
2024-01-20 23:54:03 +01:00
nor 92f7d9ded5 feat: datepicker locale support (#3878)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3878
Reviewed-by: konrad <k@knt.li>
Co-authored-by: nor <zorodey@outlook.com>
Co-committed-by: nor <zorodey@outlook.com>
2024-01-20 18:50:00 +00:00
renovate 41ccaea78b fix(deps): update dependency date-fns to v3.3.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-20 06:19:03 +00:00
renovate c5696f3e2a chore(deps): update dependency vite to v5.0.12
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-20 00:19:28 +00:00
renovate 898707664c fix(deps): update sentry-javascript monorepo to v7.94.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-19 13:19:38 +00:00
renovate d0b5bef68a chore(deps): update dependency happy-dom to v13.2.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build was killed Details
2024-01-19 00:20:24 +00:00
renovate e395d4efdb fix(deps): update dependency vue to v3.4.15
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-18 14:19:39 +00:00
renovate ce54132868 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-18 07:18:54 +00:00
renovate 07d4d1e537 fix(deps): update dependency floating-vue to v5.2.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-17 13:19:31 +00:00
renovate a701b0452e fix(deps): update dependency floating-vue to v5.1.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-17 12:18:43 +00:00
renovate af65efcd27 chore(deps): update dev-dependencies (major) (#3890)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3890
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-17 09:17:35 +00:00
renovate dc2afb9e8d chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-17 07:18:31 +00:00
WofWca e123d4f825 chore(perf): import some modules dynamically (#3179)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3179
Reviewed-by: konrad <k@knt.li>
Co-authored-by: WofWca <wofwca@protonmail.com>
Co-committed-by: WofWca <wofwca@protonmail.com>
2024-01-16 14:24:24 +00:00
renovate b72c963256 fix(deps): update dependency vue to v3.4.14
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-01-16 12:10:13 +00:00
renovate 149bbf17eb fix(deps): update dependency floating-vue to v5.1.0
continuous-integration/drone/push Build is passing Details
2024-01-16 12:01:54 +00:00
renovate 265d60cf42 fix(deps): update vueuse to v10.7.2
continuous-integration/drone/push Build is failing Details
2024-01-16 12:01:44 +00:00
renovate 23c9f51e73 fix(deps): update dependency sortablejs to v1.15.2
continuous-integration/drone/push Build is passing Details
2024-01-16 12:01:11 +00:00
renovate ff697d0c7a chore(deps): update dev-dependencies
continuous-integration/drone/push Build is passing Details
2024-01-16 11:50:52 +00:00
renovate 00588cf59f chore(deps): pin node.js (#3895)
continuous-integration/drone/push Build is failing Details
Reviewed-on: #3895
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-16 11:49:00 +00:00
renovate 01089f4f3d fix(deps): update tiptap to v2.1.16 (#3892)
continuous-integration/drone/push Build was killed Details
Reviewed-on: #3892
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-16 11:17:28 +00:00
kolaente a7461d1ddd
chore(deps): increase renovate timeout
continuous-integration/drone/push Build is passing Details
2024-01-16 12:15:04 +01:00
kolaente a451189bb6
fix(test): make date assertion not brittle
continuous-integration/drone/push Build is passing Details
2024-01-16 10:36:29 +01:00
kolaente bf9af27fc3
fix(task): update due date when marking a task done
continuous-integration/drone/push Build is failing Details
2024-01-15 23:33:02 +01:00
kolaente 5619fda0f2
fix(task): bubble date changes from the picker up
continuous-integration/drone/push Build is failing Details
Resolves https://github.com/go-vikunja/frontend/issues/142
2024-01-15 23:23:57 +01:00
kolaente 167953b26b
fix(editor): use higher-contrast colors for links and code
continuous-integration/drone/push Build is passing Details
2024-01-15 22:11:24 +01:00
kolaente 664bf0a5f4
fix(tasks): make sure tasks show up if their parent task is not available in the current view
continuous-integration/drone/push Build is passing Details
Related https://github.com/go-vikunja/frontend/issues/136
Related https://community.vikunja.io/t/subtasks-are-hidden-when-parent-tasks-are-moved/1911
2024-01-15 21:46:47 +01:00
kolaente 5e991f3024
fix: lint
continuous-integration/drone/push Build is passing Details
2024-01-15 16:21:00 +01:00
kolaente 28050d9cd5
fix(labels): make color reset work
continuous-integration/drone/push Build is failing Details
2024-01-15 14:00:08 +01:00
kolaente e94b71d577
fix(editor): list icons
continuous-integration/drone/push Build is passing Details
2024-01-15 13:39:17 +01:00
renovate 336ce217d3 chore(deps): update node.js to v20.11 (#3888)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3888
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-11 11:30:14 +00:00
Frederick [Bot] ce01085951 chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-01-11 00:11:57 +00:00
kolaente 96a6d43a3f
fix(quick add magic): ensure month is removed from task text
continuous-integration/drone/push Build is passing Details
Resolves #3874
2024-01-10 23:54:42 +01:00
kolaente 13d63e34aa
fix(task): don't immediately re-trigger date change when nothing changed
continuous-integration/drone/push Build is passing Details
Resolves https://community.vikunja.io/t/reminder-duplication/76/21?u=kolaente
2024-01-10 23:27:14 +01:00
renovate a8441c72b8 fix(deps): update dependency vue to v3.4.8 (#3886)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3886
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 20:16:50 +00:00
renovate 230fa6ce66 fix(deps): update dependency floating-vue to v5 (#3887)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3887
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 18:18:53 +00:00
renovate 069c491fbd fix(deps): update sentry-javascript monorepo to v7.93.0 (#3859)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3859
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 16:01:02 +00:00
renovate a9eae95d67 chore(deps): update pnpm to v8.14.1 (#3885)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3885
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 15:57:10 +00:00
renovate 50502d9d11 fix(deps): update vueuse to v10.7.1 (#3872)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3872
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 15:04:27 +00:00
renovate 18af6edc82 fix(deps): update tiptap to v2.1.15 (#3884)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3884
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 14:49:03 +00:00
renovate d048b61eb3 fix(deps): update dependency floating-vue to v2.0.0 (#3883)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3883
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 14:36:14 +00:00
renovate 996607e670 fix(deps): update dependency dompurify to v3.0.8 (#3881)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3881
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 14:35:50 +00:00
renovate e33ebe1831 fix(deps): update dependency vue-i18n to v9.9.0 (#3880)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3880
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 14:27:02 +00:00
renovate 557b0ffec7 chore(deps): update dependency node to v20.11.0
continuous-integration/drone/push Build is passing Details
2024-01-10 12:04:17 +00:00
renovate dae6cdb9d7 fix(deps): update dependency @kyvg/vue3-notification to v3.1.3 (#3864)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3864
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 11:59:31 +00:00
renovate 158e4d690f chore(deps): update dev-dependencies (#3861)
continuous-integration/drone/push Build is failing Details
Reviewed-on: #3861
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 11:51:20 +00:00
renovate 691eb84a99 fix(deps): update dependency date-fns to v3 (#3857)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3857
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 11:29:48 +00:00
renovate 698ee7e163 fix(deps): update dependency axios to v1.6.5 (#3871)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3871
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 11:28:28 +00:00
renovate ce822573df fix(deps): update dependency vue to v3.4.7 (#3873)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3873
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-01-10 11:19:30 +00:00
renovate 198abee01d chore(deps): update pnpm to v8.14.0
continuous-integration/drone/push Build is passing Details
2024-01-10 10:59:39 +00:00
Frederick [Bot] e5bea087be chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-01-09 00:10:45 +00:00
Frederick [Bot] 4956fbb669 chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-01-08 11:50:29 +00:00
kolaente 0351148288
fix(ci): use working crowdin image
continuous-integration/drone/push Build is passing Details
2024-01-07 20:20:14 +01:00
66 changed files with 1904 additions and 1636 deletions

View File

@ -42,7 +42,7 @@ steps:
# - .cache
- name: dependencies
image: node:20.10-alpine
image: node:20.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -55,7 +55,7 @@ steps:
# - restore-cache
- name: lint
image: node:20.10-alpine
image: node:20.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -66,7 +66,7 @@ steps:
- dependencies
- name: build-prod
image: node:20.10-alpine
image: node:20.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -77,7 +77,7 @@ steps:
- dependencies
- name: test-unit
image: node:20.10-alpine
image: node:20.11.0-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.10-alpine
image: node:20.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -202,7 +202,7 @@ steps:
# - .cache
- name: build
image: node:20.10-alpine
image: node:20.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -285,7 +285,7 @@ steps:
# - .cache
- name: build
image: node:20.10-alpine
image: node:20.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -486,7 +486,7 @@ trigger:
steps:
- name: download
pull: always
image: ghcr.io/lcomrade/lcomrade/drone-crowdin-v2:latest
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
settings:
crowdin_key:
from_secret: crowdin_key
@ -520,7 +520,7 @@ steps:
- name: upload
pull: always
image: ghcr.io/lcomrade/lcomrade/drone-crowdin-v2:latest
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
depends_on:
- clone
settings:
@ -532,6 +532,6 @@ steps:
src/i18n/lang/en.json: en.json
---
kind: signature
hmac: 1b78e92ee8a9aa94df14c35ea544abc0bec991ec59465c328a6aaa6cea4430b3
hmac: a044c7c4db3c2a11299d4d118397e9d25be36db241723a1bbd0a2f9cc90ffdac
...

2
.nvmrc
View File

@ -1 +1 @@
20.10.0
20.11.0

View File

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

View File

@ -541,6 +541,86 @@ describe('Task', () => {
.should('contain', 'Success')
})
it('Can set a due date to a specific date for a task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
done: false,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button')
.contains('Set Due Date')
.click()
cy.get('.task-view .columns.details .column')
.contains('Due Date')
.get('.date-input .datepicker .show')
.click()
cy.get('.datepicker-popup .flatpickr-innerContainer .flatpickr-days .flatpickr-day.today')
.click()
cy.get('[data-cy="closeDatepicker"]')
.contains('Confirm')
.click()
const today = new Date()
const day = today.toLocaleString('default', {day: '2-digit'})
const month = today.toLocaleString('default', {month: 'short'})
const year = today.toLocaleString('default', {year: 'numeric'})
const date = `${day} ${month} ${year}, 12:00:00`
cy.get('.task-view .columns.details .column')
.contains('Due Date')
.get('.date-input .datepicker-popup')
.should('not.exist')
cy.get('.task-view .columns.details .column')
.contains('Due Date')
.get('.date-input')
.should('contain.text', date)
cy.get('.global-notification')
.should('contain', 'Success')
})
it('Can change a due date to a specific date for a task', () => {
const dueDate = new Date()
dueDate.setHours(12)
dueDate.setMinutes(0)
dueDate.setSeconds(0)
dueDate.setDate(1)
const tasks = TaskFactory.create(1, {
id: 1,
done: false,
due_date: dueDate.toISOString(),
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button')
.contains('Set Due Date')
.click()
cy.get('.task-view .columns.details .column')
.contains('Due Date')
.get('.date-input .datepicker .show')
.click()
cy.get('.datepicker-popup .flatpickr-innerContainer .flatpickr-days .flatpickr-day.today')
.click()
cy.get('[data-cy="closeDatepicker"]')
.contains('Confirm')
.click()
const today = new Date()
const day = today.toLocaleString('default', {day: '2-digit'})
const month = today.toLocaleString('default', {month: 'short'})
const year = today.toLocaleString('default', {year: 'numeric'})
const date = `${day} ${month} ${year}, 12:00:00`
cy.get('.task-view .columns.details .column')
.contains('Due Date')
.get('.date-input .datepicker-popup')
.should('not.exist')
cy.get('.task-view .columns.details .column')
.contains('Due Date')
.get('.date-input')
.should('contain.text', date)
cy.get('.global-notification')
.should('contain', 'Success')
})
it('Can set a reminder', () => {
TaskReminderFactory.truncate()
const tasks = TaskFactory.create(1, {
@ -645,7 +725,7 @@ describe('Task', () => {
.click()
cy.get('.reminder-options-popup .card-content .reminder-period input')
.first()
.type('10')
.type('{selectall}10')
cy.get('.reminder-options-popup .card-content .reminder-period select')
.first()
.select('days')
@ -771,7 +851,7 @@ describe('Task', () => {
.should('exist')
})
it.only('Can check items off a checklist', () => {
it('Can check items off a checklist', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: `
@ -858,7 +938,7 @@ describe('Task', () => {
method: 'PUT',
url: `${Cypress.env('API_URL')}/tasks/${tasks[0].id}/attachments`,
headers: {
'Authorization': `Bearer ${window.localStorage.getItem('token')}`,
'Authorization': `Bearer ${window.localStorage.getItem('token')}`,
'Content-Type': 'multipart/form-data',
},
body: formData,

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.12.1",
"packageManager": "pnpm@8.14.3",
"keywords": [
"todo",
"productivity",
@ -52,57 +52,57 @@
"@github/hotkey": "3.1.0",
"@infectoone/vue-ganttastic": "2.2.0",
"@intlify/unplugin-vue-i18n": "2.0.0",
"@kyvg/vue3-notification": "3.1.2",
"@sentry/tracing": "7.88.0",
"@sentry/vue": "7.88.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",
"@kyvg/vue3-notification": "3.1.3",
"@sentry/tracing": "7.95.0",
"@sentry/vue": "7.95.0",
"@tiptap/core": "2.1.16",
"@tiptap/extension-blockquote": "2.1.16",
"@tiptap/extension-bold": "2.1.16",
"@tiptap/extension-bullet-list": "2.1.16",
"@tiptap/extension-code": "2.1.16",
"@tiptap/extension-code-block-lowlight": "2.1.16",
"@tiptap/extension-document": "2.1.16",
"@tiptap/extension-dropcursor": "2.1.16",
"@tiptap/extension-gapcursor": "2.1.16",
"@tiptap/extension-hard-break": "2.1.16",
"@tiptap/extension-heading": "2.1.16",
"@tiptap/extension-history": "2.1.16",
"@tiptap/extension-horizontal-rule": "2.1.16",
"@tiptap/extension-image": "2.1.16",
"@tiptap/extension-italic": "2.1.16",
"@tiptap/extension-link": "2.1.16",
"@tiptap/extension-list-item": "2.1.16",
"@tiptap/extension-ordered-list": "2.1.16",
"@tiptap/extension-paragraph": "2.1.16",
"@tiptap/extension-placeholder": "2.1.16",
"@tiptap/extension-strike": "2.1.16",
"@tiptap/extension-table": "2.1.16",
"@tiptap/extension-table-cell": "2.1.16",
"@tiptap/extension-table-header": "2.1.16",
"@tiptap/extension-table-row": "2.1.16",
"@tiptap/extension-task-item": "2.1.16",
"@tiptap/extension-task-list": "2.1.16",
"@tiptap/extension-text": "2.1.16",
"@tiptap/extension-typography": "2.1.16",
"@tiptap/extension-underline": "2.1.16",
"@tiptap/pm": "2.1.16",
"@tiptap/suggestion": "2.1.16",
"@tiptap/vue-3": "2.1.16",
"@types/is-touch-device": "1.0.2",
"@types/lodash.clonedeep": "4.5.9",
"@vueuse/core": "10.7.0",
"@vueuse/router": "10.7.0",
"axios": "1.6.5",
"@vueuse/core": "10.7.2",
"@vueuse/router": "10.7.2",
"axios": "1.6.6",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"date-fns": "2.30.0",
"date-fns": "3.3.1",
"dayjs": "1.11.10",
"dompurify": "3.0.6",
"dompurify": "3.0.8",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
"floating-vue": "2.0.0-beta.24",
"floating-vue": "5.2.0",
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
@ -110,25 +110,25 @@
"pinia": "2.1.7",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.1",
"sortablejs": "1.15.2",
"tippy.js": "6.3.7",
"ufo": "1.3.2",
"vue": "3.3.13",
"vue": "3.4.15",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.3",
"vue-i18n": "9.8.0",
"vue-i18n": "9.9.0",
"vue-router": "4.2.5",
"workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.5",
"@cypress/vite-dev-server": "5.0.6",
"@cypress/vite-dev-server": "5.0.7",
"@cypress/vue": "6.0.0",
"@faker-js/faker": "8.3.1",
"@histoire/plugin-screenshot": "0.17.6",
"@histoire/plugin-vue": "0.17.6",
"@rushstack/eslint-patch": "1.6.1",
"@histoire/plugin-screenshot": "0.17.8",
"@histoire/plugin-vue": "0.17.8",
"@rushstack/eslint-patch": "1.7.0",
"@tsconfig/node18": "18.2.2",
"@types/codemirror": "5.60.15",
"@types/dompurify": "3.0.5",
@ -136,44 +136,44 @@
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.10.5",
"@types/node": "20.11.6",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.7",
"@typescript-eslint/eslint-plugin": "6.15.0",
"@typescript-eslint/parser": "6.15.0",
"@typescript-eslint/eslint-plugin": "6.19.1",
"@typescript-eslint/parser": "6.19.1",
"@vitejs/plugin-legacy": "5.2.0",
"@vitejs/plugin-vue": "4.5.2",
"@vitejs/plugin-vue": "5.0.3",
"@vue/eslint-config-typescript": "12.0.0",
"@vue/test-utils": "2.4.3",
"@vue/tsconfig": "0.5.1",
"autoprefixer": "10.4.16",
"autoprefixer": "10.4.17",
"browserslist": "4.22.2",
"caniuse-lite": "1.0.30001570",
"caniuse-lite": "1.0.30001579",
"css-has-pseudo": "6.0.1",
"csstype": "3.1.3",
"cypress": "13.6.1",
"esbuild": "0.19.10",
"cypress": "13.6.3",
"esbuild": "0.19.12",
"eslint": "8.56.0",
"eslint-plugin-vue": "9.19.2",
"happy-dom": "12.10.3",
"histoire": "0.17.6",
"postcss": "8.4.32",
"eslint-plugin-vue": "9.20.1",
"happy-dom": "13.3.1",
"histoire": "0.17.8",
"postcss": "8.4.33",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.1",
"postcss-preset-env": "9.3.0",
"rollup": "4.9.1",
"rollup-plugin-visualizer": "5.11.0",
"sass": "1.69.5",
"rollup": "4.9.6",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.70.0",
"start-server-and-test": "2.0.3",
"typescript": "5.3.3",
"vite": "5.0.10",
"vite": "5.0.12",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.17.4",
"vite-plugin-sentry": "1.3.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.0.4",
"vue-tsc": "1.8.25",
"vitest": "1.2.1",
"vue-tsc": "1.8.27",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
},

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,11 @@
"extends": [
"config:js-app"
],
"hostRules": [
{
"timeout": 600000
}
],
"packageRules": [
{
"matchPackageNames": ["happy-dom"],

View File

@ -37,8 +37,6 @@ import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import Ready from '@/components/misc/ready.vue'
import {setLanguage} from '@/i18n'
import AccountDeleteService from '@/services/accountDelete'
import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
@ -48,6 +46,9 @@ import {useBodyClass} from '@/composables/useBodyClass'
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
import DemoMode from '@/components/home/DemoMode.vue'
const importAccountDeleteService = () => import('@/services/accountDelete')
const importMessage = () => import('@/message')
const baseStore = useBaseStore()
const authStore = useAuthStore()
const router = useRouter()
@ -68,8 +69,11 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
return
}
const messageP = importMessage()
const AccountDeleteService = (await importAccountDeleteService()).default
const accountDeletionService = new AccountDeleteService()
await accountDeletionService.confirm(accountDeletionConfirm)
const {success} = await messageP
success({message: t('user.deletion.confirmSuccess')})
authStore.refreshUserInfo()
}, { immediate: true })

View File

@ -81,9 +81,8 @@ import Popup from '@/components/misc/popup.vue'
import {DATE_RANGES} from '@/components/date/dateRanges'
import BaseButton from '@/components/base/BaseButton.vue'
import DatemathHelp from '@/components/date/datemathHelp.vue'
import {useAuthStore} from '@/stores/auth'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
const authStore = useAuthStore()
const {t} = useI18n({useScope: 'global'})
const emit = defineEmits(['update:modelValue'])
@ -93,8 +92,6 @@ const props = defineProps({
},
})
// FIXME: This seems to always contain the default value - that breaks the picker
const weekStart = computed(() => authStore.settings.weekStart ?? 0)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
@ -102,9 +99,7 @@ const flatPickerConfig = computed(() => ({
enableTime: false,
wrap: true,
mode: 'range',
locale: {
firstDayOf7Days: weekStart.value,
},
locale: getFlatpickrLanguage(),
}))
const showHowItWorks = ref(false)

View File

@ -64,6 +64,15 @@ const emit = defineEmits(['update:modelValue'])
watch(
() => modelValue,
(newValue) => {
if (newValue === '' || newValue.startsWith('var(')) {
color.value = ''
return
}
if (!newValue.startsWith('#') && (newValue.length === 6 || newValue.length === 3)) {
newValue = `#${newValue}`
}
color.value = newValue
},
{immediate: true},

View File

@ -80,8 +80,8 @@ import {formatDate} from '@/helpers/time/formatDate'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {createDateFromString} from '@/helpers/time/createDateFromString'
import {useAuthStore} from '@/stores/auth'
import {useI18n} from 'vue-i18n'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
const props = defineProps({
modelValue: {
@ -105,8 +105,6 @@ watch(
{immediate: true},
)
const authStore = useAuthStore()
const weekStart = computed(() => authStore.settings.weekStart)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
@ -114,9 +112,7 @@ const flatPickerConfig = computed(() => ({
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: weekStart.value,
},
locale: getFlatpickrLanguage(),
}))
// Since flatpickr dates are strings, we need to convert them to native date objects.
@ -128,6 +124,12 @@ const flatPickrDate = computed({
return
}
if (date.value !== null) {
const oldDate = formatDate(date.value, 'yyy-LL-dd H:mm')
if (oldDate === newValue) {
return
}
}
date.value = createDateFromString(newValue)
updateData()
},
@ -155,10 +157,6 @@ function updateData() {
}
function setDate(dateString: string) {
if (date.value === null) {
date.value = new Date()
}
const interval = calculateDayInterval(dateString)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
@ -166,7 +164,6 @@ function setDate(dateString: string) {
newDate.setMinutes(0)
newDate.setSeconds(0)
date.value = newDate
flatPickrDate.value = newDate
updateData()
}

View File

@ -110,7 +110,7 @@
v-tooltip="$t('input.editor.bulletList')"
>
<span class="icon">
<icon :icon="['fa', 'fa-list-ol']"/>
<icon :icon="['fa', 'fa-list-ul']"/>
</span>
</BaseButton>
<BaseButton
@ -120,7 +120,7 @@
v-tooltip="$t('input.editor.orderedList')"
>
<span class="icon">
<icon :icon="['fa', 'fa-list-ul']"/>
<icon :icon="['fa', 'fa-list-ol']"/>
</span>
</BaseButton>
<BaseButton
@ -336,6 +336,7 @@ import {ref} from 'vue'
import {Editor} from '@tiptap/vue-3'
import BaseButton from '@/components/base/BaseButton.vue'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
const {
editor = null,
@ -353,29 +354,8 @@ function openImagePicker() {
document.getElementById('tiptap__image-upload').click()
}
function setLink() {
const previousUrl = editor.getAttributes('link').href
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null) {
return
}
// empty
if (url === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
editor
.chain()
.focus()
.extendMarkRange('link')
.setLink({href: url, target: '_blank'})
.run()
function setLink(event) {
setLinkInEditor(event.target.getBoundingClientRect(), editor)
}
</script>

View File

@ -64,6 +64,7 @@
class="tiptap__editor"
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
:editor="editor"
@click="focusIfEditing()"
/>
<input
@ -110,6 +111,7 @@
variant="secondary"
:shadow="false"
v-cy="'saveEditor'"
:disabled="!contentHasChanged"
>
{{ $t('misc.save') }}
</x-button>
@ -171,8 +173,9 @@ import XButton from '@/components/input/button.vue'
import {Placeholder} from '@tiptap/extension-placeholder'
import {eventToHotkeyString} from '@github/hotkey'
import {mergeAttributes} from '@tiptap/core'
import {createRandomID} from '@/helpers/randomId'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
@ -227,19 +230,20 @@ const CustomImage = Image.extend({
renderHTML({HTMLAttributes}) {
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()
// The url is something like /tasks/<id>/attachments/<id>
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}`
const id = 'tiptap-image-' + cacheKey
nextTick(async () => {
const img = document.getElementById(id)
if (!img) return
// The url is something like /tasks/<id>/attachments/<id>
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}`
if (typeof loadedAttachments.value[cacheKey] === 'undefined') {
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
@ -286,8 +290,18 @@ const {
const emit = defineEmits(['update:modelValue', 'save'])
const internalMode = ref<Mode>('edit')
const internalMode = ref<Mode>('preview')
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const contentHasChanged = ref<boolean>(false)
watch(
() => internalMode.value,
mode => {
if (mode === 'preview') {
contentHasChanged.value = false
}
},
)
const editor = useEditor({
content: modelValue,
@ -308,7 +322,9 @@ const editor = useEditor({
addKeyboardShortcuts() {
return {
'Mod-Enter': () => {
bubbleSave()
if (contentHasChanged.value) {
bubbleSave()
}
},
}
},
@ -420,6 +436,7 @@ function bubbleNow() {
return
}
contentHasChanged.value = true
emit('update:modelValue', editor.value?.getHTML())
}
@ -455,7 +472,7 @@ function uploadAndInsertFiles(files: File[] | FileList) {
})
}
function addImage() {
async function addImage(event) {
if (typeof uploadCallback !== 'undefined') {
const files = uploadInputRef.value?.files
@ -469,7 +486,7 @@ function addImage() {
return
}
const url = window.prompt('URL')
const url = await inputPrompt(event.target.getBoundingClientRect())
if (url) {
editor.value?.chain().focus().setImage({src: url}).run()
@ -477,34 +494,8 @@ function addImage() {
}
}
function setLink() {
const previousUrl = editor.value?.getAttributes('link').href
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null) {
return
}
// empty
if (url === '') {
editor.value
?.chain()
.focus()
.extendMarkRange('link')
.unsetLink()
.run()
return
}
// update link
editor.value
?.chain()
.focus()
.extendMarkRange('link')
.setLink({href: url, target: '_blank'})
.run()
function setLink(event) {
setLinkInEditor(event.target.getBoundingClientRect(), editor.value)
}
onMounted(async () => {
@ -558,6 +549,7 @@ function setFocusToEditor(event) {
event.target.contentEditable === 'true') {
return
}
event.preventDefault()
if (!isEditing.value && isEditEnabled) {
@ -567,6 +559,12 @@ function setFocusToEditor(event) {
editor.value?.commands.focus()
}
function focusIfEditing() {
if (isEditing.value) {
editor.value?.commands.focus()
}
}
function clickTasklistCheckbox(event) {
event.stopImmediatePropagation()
@ -671,36 +669,17 @@ watch(
line-height: 1.1;
}
a {
color: #68cef8;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
background-color: var(--grey-200);
color: var(--grey-700);
}
pre {
background: #0d0d0d;
color: #fff;
background: var(--grey-200);
color: var(--grey-700);
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
pre {
background: #0d0d0d;
color: #fff;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border-radius: $radius;
code {
color: inherit;
@ -711,7 +690,7 @@ watch(
.hljs-comment,
.hljs-quote {
color: #616161;
color: var(--grey-500);
}
.hljs-variable,
@ -724,7 +703,7 @@ watch(
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
color: var(--code-variable);
}
.hljs-number,
@ -734,23 +713,23 @@ watch(
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
color: var(--code-literal);
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
color: var(--code-symbol);
}
.hljs-title,
.hljs-section {
color: #faf594;
color: var(--code-section);
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
color: var(--code-keyword);
}
.hljs-emphasis {

View File

@ -0,0 +1,26 @@
import inputPrompt from '@/helpers/inputPrompt'
export async function setLinkInEditor(pos, editor) {
const previousUrl = editor?.getAttributes('link').href || ''
const url = await inputPrompt(pos, previousUrl)
// empty
if (url === '') {
editor
?.chain()
.focus()
.extendMarkRange('link')
.unsetLink()
.run()
return
}
// update link
editor
?.chain()
.focus()
.extendMarkRange('link')
.setLink({href: url, target: '_blank'})
.run()
}

View File

@ -1,5 +1,5 @@
<template>
<slot name="trigger" :isOpen="open" :toggle="toggle"></slot>
<slot name="trigger" :isOpen="open" :toggle="toggle" :close="close"></slot>
<div
class="popup"
:class="{
@ -8,7 +8,7 @@
}"
ref="popup"
>
<slot name="content" :isOpen="open" :toggle="toggle"/>
<slot name="content" :isOpen="open" :toggle="toggle" :close="close"/>
</div>
</template>

View File

@ -27,7 +27,7 @@
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
{{ getDisplayName(n.notification.doer) }}
</span>
<BaseButton @click="() => to(n, index)()">
<BaseButton @click="() => to(n, index)()" class="has-text-left">
{{ n.toText(userInfo) }}
</BaseButton>
</div>
@ -223,6 +223,7 @@ async function markAllRead() {
background: var(--primary);
border-radius: 100%;
margin: 0 .5rem;
flex-shrink: 0;
&.read {
background: transparent;

View File

@ -61,6 +61,8 @@ import {
import Loading from '@/components/misc/loading.vue'
import {MILLISECONDS_A_DAY} from '@/constants/date'
import {useWeekDayFromDate} from '@/helpers/time/formatDate'
import dayjs from 'dayjs'
import {useDayjsLanguageSync} from '@/i18n/useDayjsLanguageSync'
export interface GanttChartProps {
isLoading: boolean,
@ -81,8 +83,8 @@ const emit = defineEmits<{
const {tasks, filters} = toRefs(props)
// setup dayjs for vue-ganttastic
const dayjsLanguageLoading = ref(false)
// const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
// const dayjsLanguageLoading = ref(false)
const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
extendDayjs()
const ganttContainer = ref(null)

View File

@ -44,7 +44,7 @@ import flatPickr from 'vue-flatpickr-component'
import TaskService from '@/services/task'
import type {ITask} from '@/modelTypes/ITask'
import {useAuthStore} from '@/stores/auth'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
const {
modelValue,
@ -55,7 +55,6 @@ const {
const emit = defineEmits(['update:modelValue'])
const {t} = useI18n({useScope: 'global'})
const authStore = useAuthStore()
const taskService = shallowReactive(new TaskService())
const task = ref<ITask>()
@ -102,9 +101,7 @@ const flatPickerConfig = computed(() => ({
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: authStore.settings.weekStart,
},
locale: getFlatpickrLanguage(),
}))
function deferDays(days: number) {

View File

@ -51,6 +51,7 @@ import Multiselect from '@/components/input/multiselect.vue'
import type {ILabel} from '@/modelTypes/ILabel'
import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks'
import {getRandomColorHex} from '@/helpers/color/randomColor'
const props = defineProps({
modelValue: {
@ -132,7 +133,10 @@ async function createAndAddLabel(title: string) {
return
}
const newLabel = await labelStore.createLabel(new LabelModel({title}))
const newLabel = await labelStore.createLabel(new LabelModel({
title,
hexColor: getRandomColorHex(),
}))
addLabel(newLabel, false)
labels.value.push(newLabel)
success({message: t('task.label.addCreateSuccess')})

View File

@ -220,6 +220,7 @@ $task-background: var(--white);
align-items: center;
:deep(.tag),
:deep(.checklist-summary),
.assignees,
.icon,
.priority-label {
@ -227,6 +228,10 @@ $task-background: var(--white);
margin-right: .25rem;
}
:deep(.checklist-summary) {
padding-left: 0;
}
.assignees {
display: flex;
@ -295,22 +300,27 @@ $task-background: var(--white);
}
&.has-light-text {
--white: hsla(var(--white-h), var(--white-s), var(--white-l), var(--white-a)) !important;
color: var(--white);
.task-id {
color: var(--grey-200);
color: hsl(220, 13%, 91%); // grey-200;
}
.footer .icon,
.due-date,
.priority-label {
background: var(--grey-800);
background: hsl(215, 27.9%, 16.9%); // grey-800
}
.footer {
.icon svg {
fill: var(--white);
}
:deep(.checklist-summary) {
color: hsl(217.9, 10.6%, 64.9%); // grey-400
}
}
}
}

View File

@ -9,7 +9,7 @@
{{ reminderText }}
</SimpleButton>
</template>
<template #content="{isOpen, toggle}">
<template #content="{isOpen, close}">
<Card class="reminder-options-popup" :class="{'is-open': isOpen}" :padding="false">
<div class="options" v-if="activeForm === null">
<SimpleButton
@ -17,7 +17,7 @@
:key="k"
class="option-button"
:class="{'currently-active': p.relativePeriod === modelValue?.relativePeriod && modelValue?.relativeTo === p.relativeTo}"
@click="setReminderFromPreset(p, toggle)"
@click="setReminderFromPreset(p, close)"
>
{{ formatReminder(p) }}
</SimpleButton>
@ -40,20 +40,20 @@
<ReminderPeriod
v-if="activeForm === 'relative'"
v-model="reminder"
@update:modelValue="updateDataAndMaybeClose(toggle)"
@update:modelValue="updateDataAndMaybeClose(close)"
/>
<DatepickerInline
v-if="activeForm === 'absolute'"
v-model="reminderDate"
@update:modelValue="setReminderDate(toggle)"
@update:modelValue="setReminderDate(close)"
/>
<x-button
v-if="showFormSwitch !== null"
class="reminder__close-button"
:shadow="false"
@click="toggle"
@click="updateDataAndMaybeClose(close)"
>
{{ $t('misc.confirm') }}
</x-button>
@ -148,25 +148,26 @@ function updateData() {
}
}
function setReminderDate(toggle) {
function setReminderDate(close) {
reminder.value.reminder = reminderDate.value === null
? null
: new Date(reminderDate.value)
reminder.value.relativeTo = null
reminder.value.relativePeriod = 0
updateDataAndMaybeClose(toggle)
updateDataAndMaybeClose(close)
}
function setReminderFromPreset(preset, toggle) {
function setReminderFromPreset(preset, close) {
reminder.value = preset
updateData()
toggle()
close()
}
function updateDataAndMaybeClose(toggle) {
function updateDataAndMaybeClose(close) {
updateData()
if (clearAfterUpdate) {
toggle()
close()
}
}

View File

@ -55,6 +55,7 @@ import TaskReminderModel from '@/models/taskReminder'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES, type IReminderPeriodRelativeTo} from '@/types/IReminderPeriodRelativeTo'
import {useDebounceFn} from '@vueuse/core'
const {
modelValue,
@ -105,7 +106,7 @@ function updateData() {
reminder.value.relativeTo = period.value.relativeTo
reminder.value.reminder = null
emit('update:modelValue', reminder.value)
useDebounceFn(() => emit('update:modelValue', reminder.value), 1000)
}
</script>

View File

@ -288,6 +288,7 @@ async function markAsDone(checked: boolean) {
title: t('task.undo'),
callback: () => undoDone(checked),
}])
updateDueDate()
}
if (checked) {

View File

@ -4,8 +4,8 @@
* @param color
* @returns {string}
*/
export function colorFromHex(color: string) {
if (color.substring(0, 1) === '#') {
export function colorFromHex(color: string): string {
if (color !== '' && color.substring(0, 1) === '#') {
color = color.substring(1, 7)
}

View File

@ -0,0 +1,15 @@
import {useAuthStore} from '@/stores/auth'
import FlatpickrLanguages from 'flatpickr/dist/l10n'
import type { CustomLocale, key } from 'flatpickr/dist/types/locale'
export function getFlatpickrLanguage(): CustomLocale {
const authStore = useAuthStore()
const lang = authStore.settings.language
const langPair = lang.split('-')
let language = FlatpickrLanguages[lang === 'vi-vn' ? 'vn' : 'en']
if (langPair.length > 0 && FlatpickrLanguages[langPair[0] as key] !== undefined) {
language = FlatpickrLanguages[langPair[0] as key]
}
language.firstDayOfWeek = authStore.settings.weekStart ?? language.firstDayOfWeek
return language
}

View File

@ -0,0 +1,39 @@
import {createRandomID} from '@/helpers/randomId'
import tippy from 'tippy.js'
import {nextTick} from 'vue'
import {eventToHotkeyString} from '@github/hotkey'
export default function inputPrompt(pos: ClientRect, oldValue: string = ''): Promise<string> {
return new Promise((resolve) => {
const id = 'link-input-' + createRandomID()
const linkPopup = tippy('body', {
getReferenceClientRect: () => pos,
appendTo: () => document.body,
content: `<div><input class="input" placeholder="URL" id="${id}" value="${oldValue}"/></div>`,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'top-start',
allowHTML: true,
})
linkPopup[0].show()
nextTick(() => document.getElementById(id)?.focus())
document.getElementById(id)?.addEventListener('keydown', event => {
const hotkeyString = eventToHotkeyString(event)
if (hotkeyString !== 'Enter') {
return
}
const url = event.target.value
resolve(url)
linkPopup[0].hide()
})
})
}

View File

@ -15,34 +15,32 @@ interface dateFoundResult {
const monthsRegexGroup = '(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
function matchesDateExpr(text: string, dateExpr: string): boolean {
return text.match(new RegExp('(^| )' + dateExpr, 'g')) !== null
return text.match(new RegExp('(^| )' + dateExpr, 'gi')) !== null
}
export const parseDate = (text: string, now: Date = new Date()): dateParseResult => {
const lowerText: string = text.toLowerCase()
if (matchesDateExpr(lowerText, 'today')) {
if (matchesDateExpr(text, 'today')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today')
}
if (matchesDateExpr(lowerText, 'tomorrow')) {
if (matchesDateExpr(text, 'tomorrow')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('tomorrow')), 'tomorrow')
}
if (matchesDateExpr(lowerText, 'next monday')) {
if (matchesDateExpr(text, 'next monday')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextMonday')), 'next monday')
}
if (matchesDateExpr(lowerText, 'this weekend')) {
if (matchesDateExpr(text, 'this weekend')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('thisWeekend')), 'this weekend')
}
if (matchesDateExpr(lowerText, 'later this week')) {
if (matchesDateExpr(text, 'later this week')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterThisWeek')), 'later this week')
}
if (matchesDateExpr(lowerText, 'later next week')) {
if (matchesDateExpr(text, 'later next week')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterNextWeek')), 'later next week')
}
if (matchesDateExpr(lowerText, 'next week')) {
if (matchesDateExpr(text, 'next week')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week')
}
if (matchesDateExpr(lowerText, 'next month')) {
if (matchesDateExpr(text, 'next month')) {
const date: Date = new Date()
date.setDate(1)
date.setMonth(date.getMonth() + 1)
@ -52,7 +50,7 @@ export const parseDate = (text: string, now: Date = new Date()): dateParseResult
return addTimeToDate(text, date, 'next month')
}
if (matchesDateExpr(lowerText, 'end of month')) {
if (matchesDateExpr(text, 'end of month')) {
const curDate: Date = new Date()
const date: Date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
date.setHours(calculateNearestHours(date))
@ -70,7 +68,7 @@ export const parseDate = (text: string, now: Date = new Date()): dateParseResult
parsed = getDayFromText(text)
if (parsed.date !== null) {
const month = getMonthFromText(text, parsed.date)
return addTimeToDate(text, month.date, parsed.foundText)
return addTimeToDate(month.newText, month.date, parsed.foundText)
}
parsed = getDateFromTextIn(text, now)
@ -123,7 +121,7 @@ const addTimeToDate = (text: string, date: Date, previousMatch: string | null):
const replace = results !== null ? results[0] : previousMatch
return {
newText: replaceAll(text, replace, ''),
newText: replaceAll(text, replace, '').trim(),
date: date,
}
}

View File

@ -160,6 +160,7 @@
"expired": "انتهت صلاحية هذا الرمز {ago}.",
"tokenCreatedSuccess": "إليك رمز Api الجديد: {token}",
"tokenCreatedNotSeeAgain": "قم بتخزينه في مكان آمن، لن تتمكن من عرضه مرة أخرى!",
"selectAll": "Select all",
"delete": {
"header": "احذف هذا الرمز",
"text1": "هل أنت متأكد من رغبتك في حذف الرمز \"{token}؟",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "Platnost tohoto tokenu vypršela {ago}.",
"tokenCreatedSuccess": "Zde je tvůj nový api token: {token}",
"tokenCreatedNotSeeAgain": "Ulož jej na zabezpečeném místě, už ho znovu neuvidíš!",
"selectAll": "Označit vše",
"delete": {
"header": "Smazat tento token",
"text1": "Opravdu chcete smazat token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "Dieses Token ist {ago} abgelaufen.",
"tokenCreatedSuccess": "Hier ist dein neues API Token: {token}",
"tokenCreatedNotSeeAgain": "Speichere es an einem sicheren Ort, du wirst es nicht mehr sehen!",
"selectAll": "Alle auswählen",
"delete": {
"header": "Dieses Token löschen",
"text1": "Bist Du sicher, dass Du das Token \"{token}\" löschen möchtest?",
@ -177,7 +178,7 @@
"title": "Lösche deinen Vikunja-Account",
"text1": "Das Löschen deines Accounts ist dauerhaft und unwiderruflich. Alle Projekte, Aufgaben und zugehörige Daten werden gelöscht.",
"text2": "Zum Fortfahren gib bitte dein Passwort ein. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"text3": "Klicks zum Fortfahren auf den Button unten. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"confirm": "Meinen Account löschen",
"requestSuccess": "Die Anfrage war erfolgreich. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"passwordRequired": "Bitte gib dein Passwort ein.",
@ -185,7 +186,7 @@
"scheduled": "Wir werden deinen Vikunja-Account am {date} ({dateSince}) löschen.",
"scheduledCancel": "Um die Löschung deines Accounts abzubrechen, klicke hier.",
"scheduledCancelText": "Um die Löschung deines Accounts abzubrechen, gib bitte dein Passwort unten ein:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelButton": "Um die Löschung deines Accounts abzubrechen, klicke unten auf den Button:",
"scheduledCancelConfirm": "Löschung meines Accounts abbrechen",
"scheduledCancelSuccess": "Wir werden deinen Account nicht löschen."
},
@ -937,7 +938,7 @@
"delete": "Diese Aufgabe löschen",
"priority": "Die Priorität dieser Aufgabe ändern",
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen",
"save": "Save the current task"
"save": "Aktuelle Aufgabe speichern"
},
"project": {
"title": "Projektansichten",

View File

@ -160,6 +160,7 @@
"expired": "Dieses Token ist {ago} abgelaufen.",
"tokenCreatedSuccess": "Hier ist dein neues API Token: {token}",
"tokenCreatedNotSeeAgain": "Speichere es an einem sicheren Ort, du wirst es nicht mehr sehen!",
"selectAll": "Alle auswählen",
"delete": {
"header": "Dieses Token löschen",
"text1": "Bist Du sicher, dass Du das Token \"{token}\" löschen möchtest?",
@ -177,7 +178,7 @@
"title": "Lösche deinen Vikunja-Account",
"text1": "Das Löschen deines Accounts ist dauerhaft und unwiderruflich. Alle Projekte, Aufgaben und zugehörige Daten werden gelöscht.",
"text2": "Zum Fortfahren gib bitte dein Passwort ein. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"text3": "Klicks zum Fortfahren auf den Button unten. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"confirm": "Meinen Account löschen",
"requestSuccess": "Die Anfrage war erfolgreich. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"passwordRequired": "Bitte gib dein Passwort ein.",
@ -185,7 +186,7 @@
"scheduled": "Wir werden deinen Vikunja-Account am {date} ({dateSince}) löschen.",
"scheduledCancel": "Um die Löschung deines Accounts abzubrechen, klicke hier.",
"scheduledCancelText": "Um die Löschung deines Accounts abzubrechen, gib bitte dein Passwort unten ein:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelButton": "Um die Löschung deines Accounts abzubrechen, klicke unten auf den Button:",
"scheduledCancelConfirm": "Löschung meines Accounts abbrechen",
"scheduledCancelSuccess": "Wir werden deinen Account nicht löschen."
},
@ -937,7 +938,7 @@
"delete": "Diese Aufgabe löschen",
"priority": "Die Priorität dieser Aufgabe ändern",
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen",
"save": "Save the current task"
"save": "Aktuelle Aufgabe speichern"
},
"project": {
"title": "Projektansichten",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "Ez a token lejárt {ago}.",
"tokenCreatedSuccess": "Íme az új API tokenje: {token}",
"tokenCreatedNotSeeAgain": "Tárolja el biztonságos helyen, többé nem fogja látni!",
"selectAll": "Select all",
"delete": {
"header": "Token törlése",
"text1": "Biztos benne, hogy törölni akarja ezt a tokent \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "このトークンは二度と表示されません。安全な場所に保管してください。",
"selectAll": "Select all",
"delete": {
"header": "トークンの削除",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "Ten token wygasł {ago}.",
"tokenCreatedSuccess": "Oto twój nowy token: {token}",
"tokenCreatedNotSeeAgain": "Przechowuj go w bezpiecznym miejscu, nie zobaczysz go ponownie!",
"selectAll": "Wybierz wszystkie",
"delete": {
"header": "Usuń ten token",
"text1": "Czy na pewno chcesz usunąć token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "Este token expirou {ago}.",
"tokenCreatedSuccess": "Aqui está o teu novo token de API: {token}",
"tokenCreatedNotSeeAgain": "Guarda-o num local seguro, não o vais poder visualizar novamente!",
"selectAll": "Selecionar tudo",
"delete": {
"header": "Eliminar este token",
"text1": "Tens a certeza que pretendes eliminar o token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "Срок действия этого токена истёк {ago}.",
"tokenCreatedSuccess": "Ваш новый токен: {token}",
"tokenCreatedNotSeeAgain": "Сохраните его в безопасном месте, вы не увидите его снова!",
"selectAll": "Выбрать всё",
"delete": {
"header": "Удалить этот токен",
"text1": "Удалить токен «{token}»?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "Žeton je potekel pred {ago}.",
"tokenCreatedSuccess": "Tu je vaš novi API žeton: {token}",
"tokenCreatedNotSeeAgain": "Shranite ga na varno mesto, ker ga ne boste več videli!",
"selectAll": "Izberi vse",
"delete": {
"header": "Izbriši ta žeton",
"text1": "Ali ste prepričani, da želite izbrisati žeton \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Förvara den på en säker plats, du kommer aldrig att se den igen!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -5,17 +5,17 @@
"welcomeDay": "嗨~{username}",
"welcomeEvening": "中午好,{username}",
"lastViewed": "最近查看",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"addToHomeScreen": "将此应用程序添加到您的主屏幕以便更快地访问和改善体验。",
"goToOverview": "转到概览",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
"importText": "将您的项目和任务从其他服务导入 Vikunja",
"import": "将您的数据导入到 Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
"title": "此实例处于演示模式。不要将其用于真实数据!",
"everythingWillBeDeleted": "每隔一段时间,所有东西都将被删除!",
"accountWillBeDeleted": "您的账户将被删除,包括您可能创建的所有项目、任务和附件。"
},
"404": {
"title": "未找到数据",
@ -83,17 +83,17 @@
"savedSuccess": "成功更新了设置",
"emailReminders": "通过电子邮件向我发送任务提醒",
"overdueReminders": "每天给我发送我未完成任务的摘要",
"discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
"discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
"discoverableByName": "允许其他用户在搜索我的名字时将我添加为团队或项目的成员",
"discoverableByEmail": "允许其他用户在搜索我的完整电子邮件时将我添加为团队或项目的成员",
"playSoundWhenDone": "将任务标记为已完成时播放声音",
"weekStart": "一周起始日",
"weekStartSunday": "星期日",
"weekStartMonday": "星期一",
"language": "语言设置",
"defaultProject": "Default Project",
"defaultProject": "默认项目",
"timezone": "时区",
"overdueTasksRemindersTime": "逾期任务提醒邮件时间",
"filterUsedOnOverview": "Saved filter used on the overview page"
"filterUsedOnOverview": "概述页面上使用已保存过滤器"
},
"totp": {
"title": "两步验证",
@ -147,37 +147,38 @@
}
},
"apiTokens": {
"title": "API Tokens",
"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",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"title": "API 令牌",
"general": "API 令牌允许您在无需用户凭据的情况下使用 Vikunja 的 API。",
"apiDocs": "查看 api 文档",
"createAToken": "创建令牌",
"createToken": "创建令牌",
"30d": "30 天",
"60d": "60 天",
"90d": "90 天",
"permissionExplanation": "权限允许您限制 api 令牌被允许做什么。",
"titleRequired": "需要指定标题",
"expired": "Token {ago} 前到期",
"tokenCreatedSuccess": "这是您的令牌: {token}",
"tokenCreatedNotSeeAgain": "将其存储在一个安全的位置,你不会再看到它了!",
"selectAll": "全选",
"delete": {
"header": "Delete this token",
"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."
"header": "删除该令牌",
"text1": "你确定要删除令牌 {token} 吗?",
"text2": "这将撤销使用它的所有应用程序或集成的访问权。您不能回退此操作。"
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
"title": "标题",
"titlePlaceholder": "输入一个你以后能够识别的标题",
"expiresAt": "过期于",
"permissions": "权限"
}
}
},
"deletion": {
"title": "删除您的 Vikunja 帐户",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
"text1": "您账户的删除是永久性的且无法撤消。 我们将删除您的所有项目、任务以及与之相关的所有内容。",
"text2": "若要继续,请输入您的密码。您将收到一封包含更多说明的电子邮件。",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"text3": "若要继续,请按下面的按钮。您将收到一封包含进一步说明的电子邮件。",
"confirm": "删除我的帐户",
"requestSuccess": "请求成功。您将收到一封包含更多说明的电子邮件。",
"passwordRequired": "请输入密码",
@ -185,13 +186,13 @@
"scheduled": "您的 Vikunja帐户将于 {date} ({dateSince}) 完成注销删除。",
"scheduledCancel": "要取消您的帐户删除操作,请单击此处。",
"scheduledCancelText": "若要取消您的帐户删除操作,请在下面输入您的密码:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelButton": "若要取消您的账户删除,请按下面的按钮:",
"scheduledCancelConfirm": "取消删除我的帐户",
"scheduledCancelSuccess": "账户删除操作已撤销"
},
"export": {
"title": "导出 Vikunja 数据",
"description": "You can request a copy of all your Vikunja data. This includes Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"description": "您可以索取所有 Vikunja 数据的副本。 这包括项目、任务以及与它们相关的所有内容。 您可以通过迁移功能将这些数据导入到任何 Vikunja 实例中。",
"descriptionPasswordRequired": "请输入您的密码以继续。",
"request": "请求我的 Vikunja 数据副本",
"success": "已成功请求您的 Vikunja 数据!一旦准备好下载,我们将向您发送一封电子邮件。",
@ -199,182 +200,182 @@
}
},
"project": {
"archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archived",
"showArchived": "Show Archived",
"title": "Project Title",
"color": "Color",
"projects": "Projects",
"parent": "Parent Project",
"search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this project",
"shared": "Shared Projects",
"noDescriptionAvailable": "No project description is available.",
"inboxTitle": "Inbox",
"archivedMessage": "该项目已存档。 无法为其创建新任务或编辑任务。",
"archived": "已归档",
"showArchived": "显示已归档",
"title": "项目标题",
"color": "颜色",
"projects": "项目",
"parent": "父项目",
"search": "输入以搜索项目…",
"searchSelect": "点击或按下回车键以选择此项目",
"shared": "共享项目",
"noDescriptionAvailable": "没有可用的项目描述。",
"inboxTitle": "收件箱",
"create": {
"header": "New project",
"titlePlaceholder": "The project's title goes here…",
"addTitleRequired": "Please specify a title.",
"createdSuccess": "The project was successfully created.",
"addProjectRequired": "Please specify a project or set a default project in the settings."
"header": "新项目",
"titlePlaceholder": "项目标题",
"addTitleRequired": "请指定一个标题。",
"createdSuccess": "已成功创建此项目。",
"addProjectRequired": "请指定列表或在设置中设置默认列表。"
},
"archive": {
"title": "Archive \"{project}\"",
"archive": "Archive this project",
"unarchive": "Un-Archive this project",
"unarchiveText": "You will be able to create new tasks or edit it.",
"archiveText": "You won't be able to edit this project or create new tasks until you un-archive it.",
"success": "The project was successfully archived."
"title": "存档 \"{project}\"",
"archive": "存档此项目",
"unarchive": "取消存档此项目",
"unarchiveText": "您将能够创建新任务或编辑此项目",
"archiveText": "在您取消归档之前,您将无法编辑此项目或创建新任务。",
"success": "项目已成功归档。"
},
"background": {
"title": "Set project background",
"remove": "Remove Background",
"upload": "Choose a background from your pc",
"searchPlaceholder": "Search for a background…",
"poweredByUnsplash": "Powered by Unsplash",
"loadMore": "Load more photos",
"success": "The background has been set successfully!",
"removeSuccess": "The background has been removed successfully!"
"title": "设置项目背景",
"remove": "删除背景",
"upload": "从您的pc选择背景",
"searchPlaceholder": "搜索背景…",
"poweredByUnsplash": "由 Unsplash 提供技术支持",
"loadMore": "加载更多照片",
"success": "背景图设置成功",
"removeSuccess": "已移除背景"
},
"delete": {
"title": "Delete \"{project}\"",
"header": "Delete this project",
"text1": "Are you sure you want to delete this project and all of its contents?",
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
"title": "删除\"{project}\"",
"header": "删除此项目",
"text1": "确认删除此项目及其所有任务?",
"text2": "这包括所有的任务,并且无法撤销!",
"success": "项目已成功删除。",
"tasksToDelete": "此操作无法撤消!将移除大约 {count} 个任务。",
"noTasksToDelete": "此列表不包含任何任务,可以安全删除。"
},
"duplicate": {
"title": "Duplicate this project",
"label": "Duplicate",
"text": "Select a parent project which should hold the duplicated project:",
"success": "The project was successfully duplicated."
"title": "复制此项目",
"label": "复制",
"text": "选择一个已存在的命名空间以容纳该列表的副本",
"success": "项目已成功复制."
},
"edit": {
"header": "Edit This Project",
"title": "Edit \"{project}\"",
"titlePlaceholder": "The project title goes here…",
"identifierTooltip": "The project identifier can be used to uniquely identify a task across projects. You can set it to empty to disable it.",
"identifier": "Project Identifier",
"identifierPlaceholder": "The project identifier goes here…",
"description": "Description",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "Color",
"success": "The project was successfully updated."
"header": "编辑此项目",
"title": "编辑 \"{project}\"",
"titlePlaceholder": "项目标题位于此处",
"identifierTooltip": "项目标识符可以用来独特识别整个项目的任务。您可以设置为空以禁用它。",
"identifier": "项目标识符",
"identifierPlaceholder": "项目标题位于此处",
"description": "描述",
"descriptionPlaceholder": "输入此项目的描述,点击'/'获取更多选项…",
"color": "颜色",
"success": "项目更新成功。"
},
"share": {
"header": "Share this project",
"title": "Share \"{project}\"",
"share": "Share",
"header": "共享项目",
"title": "共享 \"{project}\"",
"share": "共享",
"links": {
"title": "Share Links",
"what": "What is a share link?",
"explanation": "Share Links allow you to easily share a project with other users who don't have an account on Vikunja.",
"create": "Create a new link share",
"name": "Name (optional)",
"namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.",
"password": "Password (optional)",
"passwordExplanation": "When signing in, the user will be required to enter this password.",
"noName": "No name set",
"remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
"createSuccess": "The link share was successfully created.",
"deleteSuccess": "The link share was successfully deleted",
"view": "View",
"sharedBy": "Shared by {0}"
"title": "共享链接",
"what": "什么是共享链接?",
"explanation": "共享链接使您能够轻松地与其他未注册账户的访客共享一个列表。",
"create": "创建一个新的共享链接",
"name": "共享链接名称 (可选)",
"namePlaceholder": "例如:Lorem Ipsum",
"nameExplanation": "此共享链接中的所有动作都将显示该名称。",
"password": "密码 (可选)",
"passwordExplanation": "验证时,用户需要输入此密码。",
"noName": "未设置名称",
"remove": "删除链接共享",
"removeText": "您确定要删除此链接共享吗?它将无法使用此链接共享访问此项目。 这不能撤消!",
"createSuccess": "列表共享链接成功创建。",
"deleteSuccess": "共享链接已删除",
"view": "查看",
"sharedBy": "{0} 分享"
},
"userTeam": {
"typeUser": "user | users",
"typeTeam": "team | teams",
"shared": "Shared with these {type}",
"you": "You",
"notShared": "Not shared with any {type} yet.",
"removeHeader": "Remove a {type} from the {sharable}",
"removeText": "Are you sure you want to remove this {sharable} from the {type}? This cannot be undone!",
"removeSuccess": "The {sharable} was successfully removed from the {type}.",
"addedSuccess": "The {type} was successfully added.",
"updatedSuccess": "The {type} was successfully added."
"typeUser": "用户名 | 用户",
"typeTeam": "团队 | 团队",
"shared": "与 {type} 共享",
"you": "",
"notShared": "尚未与任何 {type} 共享。",
"removeHeader": "从 {sharable} 中移除一个 {type}",
"removeText": "确定要将 {sharable} 从 {type} 中删除吗?此操作无法撤销。",
"removeSuccess": "已成功将 {sharable} 从 {type} 中移除",
"addedSuccess": "{type} 已成功添加。",
"updatedSuccess": "{type} 已成功添加。"
},
"right": {
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
"title": "权限",
"read": "只读",
"readWrite": "可读并可写",
"admin": "管理员"
},
"attributes": {
"link": "Link",
"delete": "Delete"
"link": "链接",
"delete": "删除"
}
},
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This project is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
"title": "列表",
"add": "添加",
"addPlaceholder": "添加新任务",
"empty": "此项目目前为空。",
"newTaskCta": "新建任务。",
"editTask": "编辑任务"
},
"gantt": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
"size": "Size",
"default": "Default",
"month": "Month",
"day": "Day",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
"title": "甘特图",
"showTasksWithoutDates": "显示未设定日期的任务",
"size": "时间粒度",
"default": "默认",
"month": "",
"day": "",
"hour": "",
"range": "日期范围",
"noDates": "此任务没有设定日期。"
},
"table": {
"title": "Table",
"columns": "Columns"
"title": "表格",
"columns": ""
},
"kanban": {
"title": "Kanban",
"limit": "Limit: {limit}",
"noLimit": "Not Set",
"doneBucket": "Done bucket",
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"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…",
"addTask": "Add a task",
"addAnotherTask": "Add another task",
"addBucket": "Create a new bucket",
"addBucketPlaceholder": "Enter the new bucket title…",
"deleteHeaderBucket": "Delete the bucket",
"deleteBucketText1": "Are you sure you want to delete this bucket?",
"deleteBucketText2": "This will not delete any tasks but move them into the default bucket.",
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
"collapse": "Collapse this bucket"
"title": "看板",
"limit": "限制: {limit}",
"noLimit": "未设置",
"doneBucket": "已完成的桶数",
"doneBucketHint": "移入此存储桶的所有任务将自动标记为已完成。",
"doneBucketHintExtended": "所有移入已完成存储桶的任务都将自动标记为已完成。 从其他位置标记为已完成的任务也将被移动。",
"doneBucketSavedSuccess": "已完成存储桶已保存成功。",
"defaultBucket": "默认桶",
"defaultBucketHint": "当创建任务时没有指定一个桶,它们将被添加到此桶中。",
"defaultBucketSavedSuccess": "默认存储桶已成功保存。",
"deleteLast": "无法删除最后一个存储桶。",
"addTaskPlaceholder": "输入新任务标题…",
"addTask": "添加任务",
"addAnotherTask": "添加另一个任务",
"addBucket": "创建一个新的存储桶。",
"addBucketPlaceholder": "输入新的存储桶标题…",
"deleteHeaderBucket": "删除存储桶",
"deleteBucketText1": "您确定要删除此存储桶吗?",
"deleteBucketText2": "这不会删除任何任务,而是将其移动到默认存储桶中。",
"deleteBucketSuccess": "存储桶已删除。",
"bucketTitleSavedSuccess": "存储桶标题已保存。",
"bucketLimitSavedSuccess": "存储桶限制已保存。",
"collapse": "折叠此存储桶"
},
"pseudo": {
"favorites": {
"title": "Favorites"
"title": "收藏"
}
},
"webhooks": {
"title": "Webhooks",
"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",
"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.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
"title": "Webhook",
"targetUrl": "目标 Url",
"targetUrlInvalid": "请提供有效的URL。",
"events": "事件",
"eventsHint": "选择此 webhook 应该接收更新的所有事件 (在当前项目内)。",
"mustSelectEvents": "您必须选择至少一个事件。",
"delete": "删除此 webhook",
"deleteText": "您确定要删除此Webhook吗外部目标将不再收到其事件通知。",
"deleteSuccess": "Webhook 已成功删除。",
"create": "创建 Webhook",
"secret": "密钥",
"secretHint": "如果提供了 webhook 目标 URL 的所有请求都将使用 HMAC签名。",
"secretDocs": "查看文档了解如何使用秘密的更多详情。"
}
},
"filters": {
@ -384,7 +385,7 @@
"title": "标题",
"titlePlaceholder": "填写筛选器标题",
"description": "描述信息",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"descriptionPlaceholder": "在此处添加此过滤器的描述,点击'/'获取更多选项…",
"includeNulls": "包含没有设置值的任务",
"requireAll": "要求所有筛选器为真才能显示任务",
"showDoneTasks": "显示已完成的任务",
@ -398,9 +399,9 @@
},
"create": {
"title": "新保存的过滤器",
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
"description": "保存的过滤器是一个虚拟工程,每次访问时都从一组过滤器中计算。",
"action": "创建新保存的过滤器",
"titleRequired": "Please provide a title for the filter."
"titleRequired": "请为该过滤器提供名称。"
},
"delete": {
"header": "删除此保存的过滤器",
@ -413,7 +414,7 @@
}
},
"migrate": {
"title": "Import from other services",
"title": "从其他服务导入",
"titleService": "从 {name} 导入您的数据到 Vikunja",
"import": "导入数据到 Vikunja",
"description": "点击下面的第三方服务的徽标开始操作。",
@ -426,13 +427,13 @@
"confirm": "我确定, 请立即开始迁移!",
"importUpload": "请点击下面的按钮选择一个文件,将 {name} 的数据导入到 Vikunja",
"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."
"migrationStartedWillReciveEmail": "Vikunja 现在将从 {service} 导入您的列表/项目、任务、注释、提醒和文件。 由于这需要一段时间,完成后我们会向您发送一封电子邮件。 您现在可以关闭此窗口。",
"migrationInProgress": "迁移正在进行中。请等待完成。"
},
"label": {
"title": "标签",
"manage": "管理标签",
"description": "Click on a label to edit it. You can edit all labels you created, you can use all labels which are associated with a task to whose project you have access.",
"description": "点击标签进行编辑。 您可以编辑您创建的所有标签,您可以使用所有与您有访问权限的任务相关联的标签。",
"newCTA": "当前没有标签",
"search": "输入以搜索标签…",
"create": {
@ -443,7 +444,7 @@
},
"edit": {
"header": "编辑标签",
"forbidden": "You are not allowed to edit this label because you don't own it.",
"forbidden": "您无权编辑这个标签,因为您不拥有它。",
"success": "标签已更新"
},
"deleteSuccess": "标签已删除",
@ -457,7 +458,7 @@
},
"sharing": {
"authenticating": "验证中……",
"passwordRequired": "This shared project requires a password. Please enter it below:",
"passwordRequired": "此共享项目需要密码。请在下面输入:",
"error": "发生错误",
"invalidPassword": "密码错误"
},
@ -498,7 +499,7 @@
"custom": "自定义",
"id": "ID",
"created": "创建于",
"createdBy": "Created by {0}",
"createdBy": "由 {0} 创建",
"actions": "行为",
"cannotBeUndone": "此操作无法撤消!"
},
@ -517,59 +518,59 @@
"edit": "编辑",
"done": "完成",
"heading1": "一级标题",
"heading1Tooltip": "Big section heading.",
"heading1Tooltip": "大标题。",
"heading2": "二级标题",
"heading2Tooltip": "Medium section heading.",
"heading2Tooltip": "中标题。",
"heading3": "三级标题",
"heading3Tooltip": "Smaller section header.",
"heading3Tooltip": "小标题。",
"headingSmaller": "下一级标题",
"headingBigger": "上一级标题",
"bold": "粗体",
"italic": "斜体",
"strikethrough": "删除线",
"underline": "Underline",
"underline": "下划线",
"code": "代码",
"codeTooltip": "Capture a code snippet.",
"codeTooltip": "捕获代码片段",
"quote": "引用",
"quoteTooltip": "Capture a quote.",
"bulletList": "Bullet list",
"bulletListTooltip": "Create a simple bullet list.",
"unorderedList": "Unordered list",
"orderedList": "Ordered list",
"orderedListTooltip": "Create a list with numbering.",
"quoteTooltip": "捕获引文。",
"bulletList": "符号列表",
"bulletListTooltip": "创建一个简单的符号列表。",
"unorderedList": "无序列表",
"orderedList": "有序列表",
"orderedListTooltip": "创建带编号的列表。",
"cleanBlock": "清除格式",
"link": "链接",
"image": "图片",
"imageTooltip": "Upload an image from your computer.",
"imageTooltip": "从您的计算机上传图片",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
"title": "表格",
"insert": "插入表格",
"addColumnBefore": "在前面添加列",
"addColumnAfter": "在后面添加列",
"deleteColumn": "删除整列",
"addRowBefore": "在前面添加行",
"addRowAfter": "在后面添加行",
"deleteRow": "删除整行",
"deleteTable": "删除表格",
"mergeCells": "合并单元格",
"splitCell": "拆分单元格",
"toggleHeaderColumn": "切换头部列",
"toggleHeaderRow": "切换头部行",
"toggleHeaderCell": "切换头部单元格",
"mergeOrSplit": "合并或拆分",
"fixTables": "修复表格"
},
"horizontalRule": "水平线",
"horizontalRuleTooltip": "Divide a section.",
"horizontalRuleTooltip": "分割一节。",
"sideBySide": "并排",
"guide": "指南",
"text": "Text",
"textTooltip": "Just start typing with plain text.",
"taskList": "Task list",
"taskListTooltip": "Track tasks with a to-do list.",
"undo": "Undo",
"redo": "Redo",
"placeholder": "Type some text or hit '/' to see more options…"
"text": "文本",
"textTooltip": "只需开始用纯文本键入。",
"taskList": "任务列表",
"taskListTooltip": "用待办事宜列表跟踪任务。",
"undo": "撤销",
"redo": "恢复",
"placeholder": "输入一些文本或点击“/”查看更多选项…"
},
"multiselect": {
"createPlaceholder": "创建新的",
@ -599,14 +600,14 @@
"canuse": "你可以使用 Date Math 来筛选相对日期。",
"learnhow": "查看它如何工作",
"title": "Date Math",
"intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
"intro": "指定应用过滤器时 Vikunja 即时解析的相对日期。",
"expression": "每个 Date Math 表达式以锚点日期开头,可以是 {0},也可以是以 {1} 结尾的日期文本。 这个锚点日期后可以跟一个或多个数学表达式。",
"similar": "这些表达式类似于 {0} 和 {1} 提供的表达式。",
"add1Day": "加一天",
"minus1Day": "减一天",
"roundDay": "往最近的那天舍入",
"supportedUnits": "Supported time units",
"someExamples": "Examples of time expressions",
"supportedUnits": "支持的时间单位是:",
"someExamples": "时间表达式的一些例子:",
"units": {
"seconds": "秒数。",
"minutes": "分钟",
@ -635,7 +636,7 @@
"addReminder": "添加一个新的提醒…",
"doneSuccess": "待办事项已标记为完成。",
"undoneSuccess": "待办事项已标记为未完成。",
"undo": "Undo",
"undo": "撤销",
"openDetail": "查看任务详细信息",
"checklistTotal": "{checked} 项任务,共 {total} 项。",
"checklistAllDone": "一共 {total} 项任务",
@ -652,7 +653,7 @@
"chooseDueDate": "点击设定截止日期",
"chooseStartDate": "点击设置开始日期",
"chooseEndDate": "点击设定结束日期",
"move": "Move task to a different project",
"move": "将任务移动到另一个项目",
"done": "标记为已完成",
"undone": "标记为待办",
"created": "{1} 创建于 {0}",
@ -660,12 +661,12 @@
"doneAt": "已完成 {0}",
"updateSuccess": "该任务已保存",
"deleteSuccess": "任务已删除",
"belongsToProject": "This task belongs to project '{project}'",
"belongsToProject": "该任务属于项目'{project}'",
"due": "截止至 {at}",
"closePopup": "关闭弹窗",
"organization": "Organization",
"management": "Management",
"dateAndTime": "Date and time",
"organization": "机构",
"management": "管理",
"dateAndTime": "日期与时间",
"delete": {
"header": "删除此任务",
"text1": "确定要移除任务吗?",
@ -683,7 +684,7 @@
"percentDone": "设置进度",
"attachments": "添加附件",
"relatedTasks": "添加关联",
"moveProject": "Move",
"moveProject": "移动",
"color": "设置颜色",
"delete": "删除",
"favorite": "添加至收藏",
@ -710,15 +711,15 @@
"updated": "已更新"
},
"subscription": {
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTaskThroughParentProject": "你无法在此处取消订阅,因为你已通过其项目订阅了此任务。",
"subscribedProject": "你当前订阅了此项目,并将收到更改通知。",
"notSubscribedProject": "你没有订阅此项目,也不会收到更改通知。",
"subscribedTask": "你当前已订阅此任务并将收到更改通知。",
"notSubscribedTask": "你没有订阅此任务,也不会收到更改通知。",
"subscribe": "订阅",
"unsubscribe": "取消订阅",
"subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessProject": "你现在订阅了此项目",
"unsubscribeSuccessProject": "您已取消订阅此项目",
"subscribeSuccessTask": "你现在订阅了此任务",
"unsubscribeSuccessTask": "你现在已取消订阅此任务"
},
@ -743,7 +744,7 @@
"loading": "正在加载评论…",
"edited": "编辑于 {date}",
"creating": "正在创建评论…",
"placeholder": "Add your comment, hit '/' for more options…",
"placeholder": "添加您的评论,点击“/”获取更多选项…",
"comment": "评论",
"delete": "删除此评论",
"deleteText1": "确实要删除此评论吗?",
@ -757,7 +758,7 @@
"1week": "1周"
},
"description": {
"placeholder": "Enter a description, hit '/' for more options…",
"placeholder": "输入描述,点击'/'获取更多选项…",
"empty": "尚无描述。"
},
"assignee": {
@ -792,7 +793,7 @@
"new": "新任务关系",
"searchPlaceholder": "输入以搜索要添加关联的新任务...",
"createPlaceholder": "添加为新的关联任务",
"differentProject": "This task belongs to a different project.",
"differentProject": "此任务属于另一个项目。",
"noneYet": "还没有任务关联。",
"delete": "删除关联",
"deleteText1": "确定要删除此任务关联吗?",
@ -813,20 +814,20 @@
}
},
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
"before": "{type}前{amount} {unit} ",
"after": "{type}后{amount} {unit} ",
"beforeShort": "之前",
"afterShort": "之后",
"onDueDate": "截止日期",
"onStartDate": "开始日期",
"onEndDate": "结束日期",
"custom": "自定义",
"dateAndTime": "日期与时间"
},
"repeat": {
"everyDay": "每天",
"everyWeek": "每周",
"every30d": "Every 30 Days",
"every30d": "每 30 天",
"mode": "重复模式",
"monthly": "每月",
"fromCurrentDate": "从当前日期",
@ -840,7 +841,7 @@
"invalidAmount": "请输入大于 0 的数。"
},
"quickAddMagic": {
"hint": "Use magic prefixes to define due dates, assignees and other task properties.",
"hint": "使用魔法前缀来定义到期日期、受让人和其他任务属性。",
"title": "快速添加 Magic",
"intro": "创建任务时,可以使用特殊关键字直接为新创建的任务添加属性。 这能够更快地将常用属性添加到任务中。",
"multiple": "你可以多次使用此功能。",
@ -851,10 +852,10 @@
"priority1": "要设置任务的优先级,请添加数字 1-5并以 {prefix} 为前缀。",
"priority2": "数字越大,优先级越高。",
"assignees": "将任务分配给用户,请将带有 {prefix} 前缀的用户名添加到此任务中。",
"project1": "To set a project for the task to appear in, enter its name prefixed with {prefix}.",
"project2": "This will return an error if the project does not exist.",
"project3": "To use spaces, simply add a \" or ' around the project name.",
"project4": "For example: {prefix}\"Project with spaces\".",
"project1": "要设置任务显示的项目,请输入其名称,前缀为{prefix}。",
"project2": "如果项目不存在会返回错误。",
"project3": "要使用空格,只需在项目名称周围添加一个 \" 或 '。",
"project4": "例如:{prefix} “带空格的项目”。",
"dateAndTime": "日期与时间",
"date": "任何日期都将当做新任务的截止日期。 可以使用以下格式的日期:",
"dateWeekday": "任意工作日,将使用该日期的下一个日期",
@ -887,19 +888,19 @@
"delete": {
"header": "删除此团队",
"text1": "确定要删除此团队及其所有成员吗?",
"text2": "All team members will lose access to projects shared with this team. This CANNOT BE UNDONE!",
"text2": "所有团队成员都将无法访问共享给该团队的项目。并且无法撤消!",
"success": "团队已删除。"
},
"deleteUser": {
"header": "从团队中删除用户",
"text1": "确定要将此成员移出团队吗?",
"text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
"text2": "所有团队成员都将失去所有项目的访问权限,并且无法撤销!",
"success": "此用户已成功从团队中删除。"
},
"leave": {
"title": "离开团队",
"text1": "您确定要离开这个团队吗?",
"text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "您将无法访问该团队拥有的所有项目。 如果你改变主意,你需要一个团队管理员来再次添加你。",
"success": "您已经成功离开了团队。"
}
},
@ -908,7 +909,7 @@
"namePlaceholder": "团队的名字在这里……",
"nameRequired": "请指定名称。",
"description": "描述信息",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"descriptionPlaceholder": "在此描述团队,点击'/'获取更多选项…",
"admin": "管理员",
"member": "成员"
}
@ -931,20 +932,20 @@
"attachment": "向此任务添加附件。",
"related": "修改此任务的相关任务",
"color": "更改此任务的颜色",
"move": "Move this task to another project",
"move": "将此任务移至另一个项目。",
"reminder": "管理此任务的提醒",
"description": "切换编辑时的任务描述",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite",
"save": "Save the current task"
"delete": "删除此任务",
"priority": "更改此任务的优先级",
"favorite": "将此任务标记为收藏/取消收藏",
"save": "保存当前任务"
},
"project": {
"title": "Project Views",
"switchToListView": "Switch to list view",
"switchToGanttView": "Switch to gantt view",
"switchToKanbanView": "Switch to kanban view",
"switchToTableView": "Switch to table view"
"title": "项目视图",
"switchToListView": "切换到列表视图",
"switchToGanttView": "切换到甘特图",
"switchToKanbanView": "切换到看板视图",
"switchToTableView": "切换为表格视图"
},
"navigation": {
"title": "导航栏",
@ -952,11 +953,11 @@
"upcoming": "导航到即将到来的任务",
"labels": "导航到标签",
"teams": "导航到小组",
"projects": "Navigate to projects"
"projects": "导航到项目"
}
},
"update": {
"available": "There is an update available!",
"available": "有新版本可用。",
"do": "立即更新"
},
"menu": {
@ -967,15 +968,15 @@
"unarchive": "取消存档",
"setBackground": "设置背景",
"share": "共享",
"newProject": "New project",
"createProject": "Create project"
"newProject": "新项目",
"createProject": "创建项目"
},
"apiConfig": {
"url": "Vikunja URL",
"urlPlaceholder": "例如: http://localhost:3456",
"change": "换一换",
"use": "在 {0} 使用 Vikunja 安装程序",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
"error": "无法在“{domain}”找到或使用 Vikunja 安装。 请检查url格式是否正确直接访问是否可以访问然后重试。",
"success": "在 “{domain}” 上使用 Vikunja 安装程序。",
"urlRequired": "Url 是必需的。"
},
@ -987,26 +988,26 @@
"notification": {
"title": "通知",
"none": "没有任何通知。 祝你今天过得愉快!",
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen.",
"markAllRead": "Mark all notifications as read",
"markAllReadSuccess": "Successfully marked all notifications as read."
"explainer": "当您订阅项目或任务发生时,通知将会显示在这里。",
"markAllRead": "将所有通知标为已读",
"markAllReadSuccess": "成功标记所有通知为已读。"
},
"quickActions": {
"commands": "命令",
"placeholder": "输入命令或搜索…",
"hint": "You can use {project} to limit the search to a project. Combine {project} or {label} (labels) with a search query to search for a task with these labels or on that project. Use {assignee} to only search for teams.",
"hint": "可以使用 {project} 将搜索限制在项目中。 将 {project} 或 {label}(标签)与搜索查询相结合,可以搜索有这些标签或在该项目中的任务。 {assignee} 仅适用于在团队中搜索。",
"tasks": "任务",
"projects": "Projects",
"projects": "项目",
"teams": "团队",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"labels": "标签",
"newProject": "输入新项目的标题…",
"newTask": "输入新任务的标题...",
"newTeam": "输入新团队的名称...",
"createTask": "Create a task in the current project ({title})",
"createProject": "Create a project",
"createTask": "在当前项目中创建一个任务 ({title})",
"createProject": "创建一个项目",
"cmds": {
"newTask": "新建任务",
"newProject": "New project",
"newProject": "新项目",
"newTeam": "新建团队"
}
},
@ -1037,15 +1038,15 @@
"1018": "用户头像设置无效。",
"2001": "ID 不能为空或 0。",
"2002": "一些请求数据无效。",
"3001": "The project does not exist.",
"3004": "You need to have read permissions on that project to perform that action.",
"3005": "The project title cannot be empty.",
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"3001": "项目不存在",
"3004": "您需要读取该项目的权限才能执行此操作。",
"3005": "列表标题不能为空。",
"3006": "项目共享不存在。",
"3007": "具有此标识符的项目已存在。",
"3008": "该项目已存档,因此只能读取。与该项目相关的所有任务也是如此。",
"4001": "项目任务文本不能为空。",
"4002": "项目任务不存在。",
"4003": "所有批量编辑任务必须属于同一项目。",
"4004": "批量编辑任务时至少需要选择一项任务。",
"4005": "你没有权限查看此任务。",
"4006": "不能将上级任务设置为任务本身。",
@ -1064,21 +1065,21 @@
"4019": "任务筛选值无效。",
"6001": "团队名称不能为空。",
"6002": "团队不存在。",
"6004": "The team already has access to that project.",
"6004": "该团队已经可以访问该项目。",
"6005": "该用户已经是此团队的成员。",
"6006": "无法删除最后一个团队成员。",
"6007": "The team does not have access to the project to perform that action.",
"7002": "The user already has access to that project.",
"7003": "You do not have access to that project.",
"6007": "该团队没有权限访问此项目来执行操作。",
"7002": "用户已经有权访问此项目。",
"7003": "您无权访问此项目",
"8001": "此标签已存在于该任务中。",
"8002": "标签不存在。",
"8003": "你没有权限访问此标签。",
"9001": "权限无效。",
"10001": "存储桶不存在。",
"10002": "The bucket does not belong to that project.",
"10003": "You cannot remove the last bucket on a project.",
"10002": "此存储桶不属于该项目。",
"10003": "无法删除项目中的最后一个存储桶。",
"10004": "无法将任务添加到此存储桶,因为已超过此存储桶可容纳的任务限制。",
"10005": "There can be only one done bucket per project.",
"10005": "每个项目只能有一个已完成的桶。",
"11001": "已保存筛选器不存在。",
"11002": "已保存的筛选器不适用于链接共享。",
"12001": "订阅实体类型无效。",
@ -1093,13 +1094,13 @@
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
"seconds": "秒|秒",
"minutes": "分|分钟",
"hours": "时|小时",
"days": "天|天",
"weeks": "周|周",
"months": "月|月",
"years": "年|年"
}
}
}

View File

@ -160,6 +160,7 @@
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -5,7 +5,6 @@ import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import {getRandomColorHex} from '@/helpers/color/randomColor'
export default class LabelModel extends AbstractModel<ILabel> implements ILabel {
id = 0
@ -24,15 +23,21 @@ export default class LabelModel extends AbstractModel<ILabel> implements ILabel
constructor(data: Partial<ILabel> = {}) {
super()
this.assignData(data)
if (this.hexColor === '') {
this.hexColor = getRandomColorHex()
}
if (this.hexColor.substring(0, 1) !== '#') {
if (this.hexColor !== '' && !this.hexColor.startsWith('#') && !this.hexColor.startsWith('var(')) {
this.hexColor = '#' + this.hexColor
}
this.textColor = colorIsDark(this.hexColor) ? '#4a4a4a' : '#fff'
if (this.hexColor === '') {
this.hexColor = 'var(--grey-200)'
this.textColor = 'var(--grey-800)'
} else {
this.textColor = colorIsDark(this.hexColor)
// Fixed colors to avoid flipping in dark mode
? 'hsl(215, 27.9%, 16.9%)' // grey-800
: 'hsl(220, 13%, 91%)' // grey-200
}
this.createdBy = new UserModel(this.createdBy)
this.created = new Date(this.created)

View File

@ -112,6 +112,16 @@ describe('Parse Task Text', () => {
expect(result?.date?.getMonth()).toBe(tomorrow.getMonth())
expect(result?.date?.getDate()).toBe(tomorrow.getDate())
})
it('should recognize Tomorrow', () => {
const result = parseTaskText('Lorem Ipsum Tomorrow')
expect(result.text).toBe('Lorem Ipsum')
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
expect(result?.date?.getFullYear()).toBe(tomorrow.getFullYear())
expect(result?.date?.getMonth()).toBe(tomorrow.getMonth())
expect(result?.date?.getDate()).toBe(tomorrow.getDate())
})
it('should recognize next monday', () => {
const result = parseTaskText('Lorem Ipsum next monday')
@ -441,7 +451,7 @@ describe('Parse Task Text', () => {
'06/08/2021': '2021-6-8',
'6/7/21': '2021-6-7',
'27/07/2021,': null,
'2021/07/06,': '2021-7-6',
'2021/07/06': '2021-7-6',
'2021-07-06': '2021-7-6',
'27 jan': '2022-1-27',
'27/1': '2022-1-27',
@ -449,39 +459,52 @@ describe('Parse Task Text', () => {
'16/12': '2021-12-16',
'01/27': '2022-1-27',
'1/27': '2022-1-27',
'Jan 27': '2022-1-27',
'jan 27': '2022-1-27',
'Jan 27': '2022-1-27',
'feb 21': '2022-2-21',
'Feb 21': '2022-2-21',
'mar 21': '2022-3-21',
'Mar 21': '2022-3-21',
'apr 21': '2022-4-21',
'Apr 21': '2022-4-21',
'may 21': '2022-5-21',
'May 21': '2022-5-21',
'jun 21': '2022-6-21',
'Jun 21': '2022-6-21',
'jul 21': '2021-7-21',
'Jul 21': '2021-7-21',
'aug 21': '2021-8-21',
'Aug 21': '2021-8-21',
'sep 21': '2021-9-21',
'Sep 21': '2021-9-21',
'oct 21': '2021-10-21',
'Oct 21': '2021-10-21',
'nov 21': '2021-11-21',
'Nov 21': '2021-11-21',
'dec 21': '2021-12-21',
'Dec 21': '2021-12-21',
} as Record<string, string | null>
for (const c in cases) {
it(`should parse '${c}' as '${cases[c]}' with the date at the end`, () => {
const {date} = getDateFromText(`Lorem Ipsum ${c}`, now)
const {date, foundText} = getDateFromText(`Lorem Ipsum ${c}`, now)
if (date === null && cases[c] === null) {
expect(date).toBeNull()
return
}
expect(`${date?.getFullYear()}-${date?.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
expect(foundText.trim()).toBe(c)
})
it(`should parse '${c}' as '${cases[c]}' with the date at the beginning`, () => {
const {date} = getDateFromText(`${c} Lorem Ipsum`, now)
const {date, foundText} = getDateFromText(`${c} Lorem Ipsum`, now)
if (date === null && cases[c] === null) {
expect(date).toBeNull()
return
}
expect(`${date?.getFullYear()}-${date?.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
expect(foundText.trim()).toBe(c)
})
}
})
@ -532,6 +555,20 @@ describe('Parse Task Text', () => {
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe(cases[c])
})
}
it('should replace the text in title case', () => {
const {date, newText} = parseDate('Some task Mar 8th', now)
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe('2021-3-8 12:0')
expect(newText).toBe('Some task')
})
it('should replace the text in lowercase', () => {
const {date, newText} = parseDate('Some task mar 8th', now)
expect(`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`).toBe('2021-3-8 12:0')
expect(newText).toBe('Some task')
})
})
})

View File

@ -67,7 +67,7 @@ export const useLabelStore = defineStore('label', () => {
}
function setLabel(label: ILabel) {
labels.value[label.id] = label
labels.value[label.id] = {...label}
update(label)
}

View File

@ -29,6 +29,7 @@ import {useBaseStore} from '@/stores/base'
import ProjectUserService from '@/services/projectUsers'
import {useAuthStore} from '@/stores/auth'
import TaskCollectionService from '@/services/taskCollection'
import {getRandomColorHex} from '@/helpers/color/randomColor'
interface MatchedAssignee extends IUser {
match: string,
@ -337,7 +338,10 @@ export const useTaskStore = defineStore('task', () => {
let label = validateLabel(Object.values(labelStore.labels), labelTitle)
if (typeof label === 'undefined') {
// label not found, create it
const labelModel = new LabelModel({title: labelTitle})
const labelModel = new LabelModel({
title: labelTitle,
hexColor: getRandomColorHex(),
})
label = await labelStore.createLabel(labelModel)
}
return label

View File

@ -256,6 +256,13 @@
--card-border-color: var(--grey-200);
--logo-text-color: hsl(180, 1%, 15%);
// Code colors
--code-variable: #da2222;
--code-literal: #fd8a09;
--code-symbol: #0ead69;
--code-section: #3a86ff;
--code-keyword: #8338ec;
&.dark {
@media screen {
// Light mode colours reversed for dark mode
@ -311,6 +318,13 @@
--scheme-invert: var(--grey-900);
--scheme-invert-bis: var(--grey-900);
--scheme-invert-ter: var(--grey-800);
// Code colors
--code-variable: #f98181;
--code-literal: #fbbc88;
--code-symbol: #b9f18d;
--code-section: #faf594;
--code-keyword: #70cff8;
}
}
}

View File

@ -150,8 +150,8 @@ function deleteLabel(label: ILabel) {
}
function editLabelSubmit() {
return labelStore.updateLabel(labelEditLabel.value)
}
return labelStore.updateLabel(labelEditLabel.value)
}
function editLabel(label: ILabel) {
if (label.createdBy.id !== userInfo.value.id) {

View File

@ -35,7 +35,7 @@
</template>
<script setup lang="ts">
import {computed, ref} from 'vue'
import {computed, onBeforeMount, ref} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRouter} from 'vue-router'
@ -46,6 +46,7 @@ import LabelModel from '@/models/label'
import {useLabelStore} from '@/stores/labels'
import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import {getRandomColorHex} from '@/helpers/color/randomColor'
const router = useRouter()
@ -55,6 +56,8 @@ useTitle(() => t('label.create.title'))
const labelStore = useLabelStore()
const label = ref(new LabelModel())
onBeforeMount(() => label.value.hexColor = getRandomColorHex())
const showError = ref(false)
const loading = computed(() => labelStore.isLoading)

View File

@ -54,7 +54,7 @@ import {useI18n} from 'vue-i18n'
import type {RouteLocationNormalized} from 'vue-router'
import {useBaseStore} from '@/stores/base'
import {useAuthStore} from '@/stores/auth'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
@ -126,16 +126,13 @@ const flatPickerDateRange = computed<Date[]>({
const initialDateRange = [filters.value.dateFrom, filters.value.dateTo]
const {t} = useI18n({useScope: 'global'})
const authStore = useAuthStore()
const flatPickerConfig = computed<Options>(() => ({
altFormat: t('date.altFormatShort'),
altInput: true,
defaultDate: initialDateRange,
enableTime: false,
mode: 'range',
locale: {
firstDayOfWeek: authStore.settings.weekStart,
},
locale: getFlatpickrLanguage(),
}))
</script>

View File

@ -179,7 +179,23 @@ watch(
if (projectId < 0) {
return
}
tasks.value = tasks.value.filter(t => typeof t.relatedTasks?.parenttask === 'undefined')
const tasksById = {}
tasks.value.forEach(t => tasksById[t.id] = true)
tasks.value = tasks.value.filter(t => {
if (typeof t.relatedTasks?.parenttask === 'undefined') {
return true
}
// If the task is a subtask, make sure the parent task is available in the current view as well
for (const pt of t.relatedTasks.parenttask) {
if(typeof tasksById[pt.id] === 'undefined') {
return true
}
}
return false
})
},
)

View File

@ -10,14 +10,12 @@
{{ $t('project.delete.text1') }}
</p>
<p>
<strong v-if="totalTasks !== null" class="has-text-white">
{{
totalTasks > 0 ? $t('project.delete.tasksToDelete', {count: totalTasks}) : $t('project.delete.noTasksToDelete')
}}
</strong>
<Loading v-else class="is-loading-small" variant="default"/>
<p class="has-text-weight-bold" v-if="totalTasks !== null">
{{
totalTasks > 0 ? $t('project.delete.tasksToDelete', {count: totalTasks}) : $t('project.delete.noTasksToDelete')
}}
</p>
<Loading v-else class="is-loading-small" variant="default"/>
<p>
{{ $t('misc.cannotBeUndone') }}

View File

@ -10,9 +10,9 @@ import {MILLISECONDS_A_DAY} from '@/constants/date'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import {useI18n} from 'vue-i18n'
import {useAuthStore} from '@/stores/auth'
import Message from '@/components/misc/message.vue'
import type {IApiToken} from '@/modelTypes/IApiToken'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
const service = new ApiTokenService()
const tokens = ref<IApiToken[]>([])
@ -32,18 +32,16 @@ const showDeleteModal = ref<boolean>(false)
const tokenToDelete = ref<IApiToken>()
const {t} = useI18n()
const authStore = useAuthStore()
const now = new Date()
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
locale: {
firstDayOfWeek: authStore.settings.weekStart,
},
locale: getFlatpickrLanguage(),
minDate: now,
}))