Compare commits

..

1 Commits

57 changed files with 1519 additions and 2064 deletions

View File

@ -42,7 +42,7 @@ steps:
# - .cache
- name: dependencies
image: node:20.11-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -55,7 +55,7 @@ steps:
# - restore-cache
- name: lint
image: node:20.11-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -66,7 +66,7 @@ steps:
- dependencies
- name: build-prod
image: node:20.11-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -77,7 +77,7 @@ steps:
- dependencies
- name: test-unit
image: node:20.11-alpine
image: node:20.10-alpine
pull: always
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
@ -87,7 +87,7 @@ steps:
- name: typecheck
failure: ignore
image: node:20.11-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -202,7 +202,7 @@ steps:
# - .cache
- name: build
image: node:20.11-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -285,7 +285,7 @@ steps:
# - .cache
- name: build
image: node:20.11-alpine
image: node:20.10-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -486,7 +486,7 @@ trigger:
steps:
- name: download
pull: always
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
image: git.lcomrade.su/root/drone-crowdin-v2
settings:
crowdin_key:
from_secret: crowdin_key
@ -513,14 +513,14 @@ steps:
author_name: Frederick [Bot]
branch: main
commit: true
commit_message: "chore(i18n): update translations via Crowdin"
commit_message: "[skip ci] Updated translations via Crowdin"
remote: "ssh://git@kolaente.dev:9022/vikunja/frontend.git"
ssh_key:
from_secret: git_push_ssh_key
- name: upload
pull: always
image: ghcr.io/kolaente/kolaente/drone-crowdin-v2:latest
image: git.lcomrade.su/root/drone-crowdin-v2
depends_on:
- clone
settings:
@ -532,6 +532,6 @@ steps:
src/i18n/lang/en.json: en.json
---
kind: signature
hmac: c755a046a47f0b8a799a81a0c686cc4e7734938298c22b47a24e268aec3cf908
hmac: 3380c4283256eea047e6228817161991d23457d09abe9d99f06e018b1eb047f4
...

View File

@ -6,6 +6,7 @@
# (2) Comment in and adjust the values as needed.
# VITE_IS_ONLINE=true
# VITE_WORKBOX_DEBUG=false
# SENTRY_AUTH_TOKEN=YOUR_TOKEN
# SENTRY_ORG=vikunja
# SENTRY_PROJECT=frontend-oss

2
.nvmrc
View File

@ -1 +1 @@
20.11.0
20.10.0

View File

@ -9,390 +9,6 @@ All releases can be found on https://code.vikunja.io/frontend/releases.
The releases aim at the api versions which is why there are missing versions.
## [0.22.0] - 2023-12-19
### Bug Fixes
* *(api tokens)* Expiry of tokens in a number of days
* *(api tokens)* Lint
* *(api tokens)* Make deletion of old tokens work
* *(api tokens)* Show a token after it was created
* *(attachments)* Layout and coloring in dark mode
* *(auth)* Correctly redirect the user to the last visited page after login
* *(auth)* Silently discard invalid auth tokens and log the user out
* *(background)* Unsplash author credit in dark mode
* *(build)* Don't download Puppeteer when building for prod
* *(ci)* Pin used node version to 20.5 to avoid build issues
* *(ci)* Use correct secret key to push
* *(docker)* Set correct default value for custom logo url
* *(editor)* Actions styling
* *(editor)* Actually populate loaded data into the editor
* *(editor)* Add icons for clearing marks and nodes
* *(editor)* Add missing dependencies for commands
* *(editor)* Add missing dependency
* *(editor)* Add workaround for checklist tiptap bug
* *(editor)* Alignment and focus states
* *(editor)* Allow checking a checkbox even when the editor is set to read only
* *(editor)* Always set mode to preview after save
* *(editor)* Always show placeholder when empty
* *(editor)* Change description when switching between tasks
* *(editor)* Check for almost empty editor value
* *(editor)* Check for empty content
* *(editor)* Checklist button icon
* *(editor)* Commands list in dark mode
* *(editor)* Correctly resolve images in descriptions
* *(editor)* Don't check parent checkbox when child label was clicked
* *(editor)* Don't crash when the component isn't completely mounted
* *(editor)* Don't create empty "blob" files when pasting images
* *(editor)* Don't prevent typing editor focus shortcut when other instance of an editor is focused already
* *(editor)* Don't use global shortcut when anything is focused
* *(editor)* Duplicate name
* *(editor)* Duplicate name for extension
* *(editor)* Focus state
* *(editor)* Image button icon
* *(editor)* Image paste handling
* *(editor)* Keep editor open when emptying content from the outside
* *(editor)* Keep editor open when emptying content from the outside (#3852)
* *(editor)* Lint
* *(editor)* Lint
* *(editor)* List styling
* *(editor)* Make checklist indicator work again
* *(editor)* Make initial editor mode (preview/edit) work
* *(editor)* Make tests work with changed structure
* *(editor)* Permission check for table editing
* *(editor)* Placeholder showing or not showing
* *(editor)* Reset on empty
* *(editor)* Show editor if there is no content initially
* *(editor)* Use edit enable
* *(editor)* Use modelValue directly to update values in the editor
* *(filter)* Don't immediately re-trigger prepareFilter
* *(filter)* Don't prevent entering date math strings
* *(filter)* Don't show other filters in project selection in saved filter
* *(filter)* Make other filters are not available for project selection
* *(filters)* Don't allow marking a filter as favorite
* *(filters)* Incorrect translation string
* *(filters)* Infinite loop when creating filters with dates (#3061)
* *(gantt)* Open task with double click from the gantt chart
* *(gantt)* Update the gantt view when switching between projects
* *(i18n)* Add upload files config
* *(i18n)* Fall back to browser language if the configured user language is invalid
* *(i18n)* Hungarian translation
* *(kanban)* Check if doneBucketId is set
* *(kanban)* Make sure kanban cards always have text color matching their background
* *(kanban)* Opening a task from the kanban board and then reloading the page should not crash everything when then navigating back
* *(list view)* Align nested subtasks with the parent text
* *(menu)* Separate favorite and saved filter projects from other projects
* *(navigation)* Don't hide color bubble in navigation on touch devices
* *(navigation)* Show filter settings dropdown
* *(project)* Correctly show project color next to project title in list view
* *(projects)* Don't suggest to create a new task in an empty filter
* *(quick actions)* Always open quick actions with hotkey, even if other inputs are focused
* *(quick actions)* Always search for projects
* *(quick actions)* Don't show projects when searching for labels or tasks
* *(quick actions)* Invalid class prop
* *(quick actions)* Project filter
* *(quick actions)* Project search
* *(quick actions)* Search for tasks within a project when specifying a project with quick add magic
* *(quick add magic)* Annually and variants spelling
* *(quick add magic)* Headline
* *(quick add magic)* Ignore common task indention when adding multiple tasks at once
* *(quick add magic)* Repeating intervals in words
* *(settings)* Allow removing the default project via settings
* *(settings)* Move overdue remindeer time below
* *(sw)* Remove debug option via env as it would not be replaced correctly in prod builds
* *(task)* Correct spacing to task and project title
* *(task)* Correctly build task identifier
* *(task)* Don't reload the kanban board when opening a task
* *(task)* Don't reload the kanban board when opening a task
* *(task)* Duplicate attribute
* *(task)* Make sure the modal close button is not overlapped with the title field (#3256)
* *(task)* Priority label sizing and positioning in different environments
* *(task)* Priority label spacing
* *(task)* Remove wrong repeat types
* *(task)* Show related tasks form with shortcut even when there are already other related tasks
* *(task)* Use editor as preview first, then check for edit
* *(task)* Use empty description helper everywhere
* *(tasks)* Don't use the filter for upcoming when one is set for the home page
* *(tasks)* Favorited sub tasks are not shown in favorites pseudo list
* *(tasks)* Ignore empty lines when adding multiple tasks at once
* *(tasks)* Make sure tasks are fully clickable
* *(tasks)* Play pop sound directly and not from store
* *(tasks)* Prevent endless references
* *(tasks)* Reset page number when applying filters
* *(tasks)* Update api route
* *(tasks)* Update sub task relations in list view after they were created
* *(tasks)* Use mousedown event instead of click to close the task popup
* *(test)* Use correct file input
* *(user)* Allow openid users to request their deletion
* *(webhooks)* Styling* Correctly resolve kanban board in the background when moving a task ([8902c15](8902c15f7e9590da075e860f3d35939169ee246a))
* Don't render route modal when no properties are defined ([b1fe3fe](b1fe3fe29b3f7c8e3f1fa279b74f674bc63db232))
* Don't try to load buckets for project id 0 ([15ecafd](15ecafdf04391139da27f38dac9ed915d6220a9a))
* Lint ([218d724](218d72494a088b612e720ca2e9b566c0d3446579))
* Lint ([337c3e5](337c3e5e3e06a9e4928bebffda2e2f223fef398b))
* Lint ([7f2d921](7f2d92138e302188d6000632b4bc9bf053194dee))
* Lint ([99e2161](99e2161c09b1e2b08f3a907bd2e3ad2c71da87d3))
* Lint ([c01957a](c01957aae24696812c80b18c77137b5030fc757a))
* Tests ([f6d1db3](f6d1db35957c4c2fda7a58539a0a39db1b683ccb))
### Dependencies
* *(deps)* Remove unused dependencies
* *(deps)* Update dependencies
* *(deps)* Update dependencies
* *(deps)* Update dependency @fortawesome/vue-fontawesome to v3.0.5 (#3815)
* *(deps)* Update dependency @github/hotkey to v2.1.0 (#3766)
* *(deps)* Update dependency @github/hotkey to v2.1.1 (#3770)
* *(deps)* Update dependency @github/hotkey to v2.2.0 (#3809)
* *(deps)* Update dependency @github/hotkey to v2.3.0 (#3810)
* *(deps)* Update dependency @github/hotkey to v2.3.1 (#3845)
* *(deps)* Update dependency @github/hotkey to v3
* *(deps)* Update dependency @infectoone/vue-ganttastic to v2.2.0
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v0.12.2
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v1
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v1.5.0 (#3812)
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v1.6.0
* *(deps)* Update dependency @kyvg/vue3-notification to v3
* *(deps)* Update dependency @kyvg/vue3-notification to v3.1.2
* *(deps)* Update dependency @types/is-touch-device to v1.0.1 (#3786)
* *(deps)* Update dependency @types/is-touch-device to v1.0.2 (#3816)
* *(deps)* Update dependency @types/lodash.clonedeep to v4.5.8 (#3787)
* *(deps)* Update dependency @types/lodash.clonedeep to v4.5.9 (#3817)
* *(deps)* Update dependency @types/node to v18.17.0
* *(deps)* Update dependency @types/node to v20 (#3796)
* *(deps)* Update dependency @types/sortablejs to v1.15.4 (#3788)
* *(deps)* Update dependency @types/sortablejs to v1.15.5 (#3818)
* *(deps)* Update dependency @vueuse/core to v10.3.0
* *(deps)* Update dependency @vueuse/core to v10.4.0 (#3723)
* *(deps)* Update dependency axios to v1.5.1
* *(deps)* Update dependency axios to v1.6.0 (#3801)
* *(deps)* Update dependency axios to v1.6.2 (#3820)
* *(deps)* Update dependency caniuse-lite to v1.0.30001514
* *(deps)* Update dependency codemirror to v5.65.14
* *(deps)* Update dependency dayjs to v1.11.10 (#3753)
* *(deps)* Update dependency dompurify to v3.0.5
* *(deps)* Update dependency dompurify to v3.0.6 (#3754)
* *(deps)* Update dependency eslint to v8.52.0 (#3785)
* *(deps)* Update dependency highlight.js to v11.9.0 (#3763)
* *(deps)* Update dependency lowlight to v2.9.0 (#3789)
* *(deps)* Update dependency marked to v5.1.1
* *(deps)* Update dependency marked to v9
* *(deps)* Update dependency marked to v9.1.0 (#3760)
* *(deps)* Update dependency marked to v9.1.1 (#3768)
* *(deps)* Update dependency marked to v9.1.2 (#3774)
* *(deps)* Update dependency node (#3797)
* *(deps)* Update dependency node (#3834)
* *(deps)* Update dependency node to v18.18.0
* *(deps)* Update dependency node to v18.18.1
* *(deps)* Update dependency node to v18.18.2
* *(deps)* Update dependency pinia to v2.1.6
* *(deps)* Update dependency pinia to v2.1.7 (#3771)
* *(deps)* Update dependency sass to v1.69.2 (#3767)
* *(deps)* Update dependency sortablejs to v1.15.1 (#3841)
* *(deps)* Update dependency ufo to v1.2.0
* *(deps)* Update dependency ufo to v1.3.1
* *(deps)* Update dependency ufo to v1.3.2 (#3824)
* *(deps)* Update dependency vite to v4.4.2
* *(deps)* Update dependency vite to v4.4.3
* *(deps)* Update dependency vue to v3.3.10 (#3843)
* *(deps)* Update dependency vue to v3.3.13
* *(deps)* Update dependency vue to v3.3.5 (#3782)
* *(deps)* Update dependency vue to v3.3.6 (#3784)
* *(deps)* Update dependency vue to v3.3.7 (#3799)
* *(deps)* Update dependency vue to v3.3.8 (#3814)
* *(deps)* Update dependency vue to v3.3.9 (#3837)
* *(deps)* Update dependency vue-i18n to v9.5.0
* *(deps)* Update dependency vue-i18n to v9.6.0 (#3800)
* *(deps)* Update dependency vue-i18n to v9.6.1 (#3803)
* *(deps)* Update dependency vue-i18n to v9.6.5 (#3807)
* *(deps)* Update dependency vue-i18n to v9.7.0 (#3825)
* *(deps)* Update dependency vue-i18n to v9.8.0 (#3833)
* *(deps)* Update dependency vue-router to v4.2.5 (#3755)
* *(deps)* Update dessant/repo-lockdown action to v4
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies (#3721)
* *(deps)* Update dev-dependencies (#3726)
* *(deps)* Update dev-dependencies (#3740)
* *(deps)* Update dev-dependencies (#3746)
* *(deps)* Update dev-dependencies (#3747)
* *(deps)* Update dev-dependencies (#3757)
* *(deps)* Update dev-dependencies (#3761)
* *(deps)* Update dev-dependencies (#3769)
* *(deps)* Update dev-dependencies (#3776)
* *(deps)* Update dev-dependencies (#3780)
* *(deps)* Update dev-dependencies (#3793)
* *(deps)* Update dev-dependencies (#3802)
* *(deps)* Update dev-dependencies (#3806)
* *(deps)* Update dev-dependencies (#3811)
* *(deps)* Update dev-dependencies (#3813)
* *(deps)* Update dev-dependencies (#3821)
* *(deps)* Update dev-dependencies (#3826)
* *(deps)* Update dev-dependencies (#3828)
* *(deps)* Update dev-dependencies (#3829)
* *(deps)* Update dev-dependencies (#3835)
* *(deps)* Update dev-dependencies (#3842)
* *(deps)* Update dev-dependencies (#3846)
* *(deps)* Update dev-dependencies (#3856)
* *(deps)* Update dev-dependencies (major) (#3741)
* *(deps)* Update dev-dependencies (major) (#3827)
* *(deps)* Update dev-dependencies to v6
* *(deps)* Update flake
* *(deps)* Update font awesome to v6.4.2
* *(deps)* Update font awesome to v6.5.1 (#3839)
* *(deps)* Update lockfile
* *(deps)* Update lockfile
* *(deps)* Update lockfile
* *(deps)* Update lockfile
* *(deps)* Update lockfile
* *(deps)* Update lockfile
* *(deps)* Update node.js to v18.17.0
* *(deps)* Update node.js to v18.17.1
* *(deps)* Update node.js to v20.7 (#3736)
* *(deps)* Update node.js to v20.8 (#3756)
* *(deps)* Update pnpm to v8.10.2
* *(deps)* Update pnpm to v8.10.5
* *(deps)* Update pnpm to v8.11.0
* *(deps)* Update pnpm to v8.12.1
* *(deps)* Update pnpm to v8.6.12
* *(deps)* Update pnpm to v8.6.7
* *(deps)* Update pnpm to v8.6.8
* *(deps)* Update pnpm to v8.6.9
* *(deps)* Update pnpm to v8.7.0
* *(deps)* Update pnpm to v8.8.0
* *(deps)* Update pnpm to v8.9.0
* *(deps)* Update pnpm to v8.9.2
* *(deps)* Update sentry-javascript monorepo to v7.58.0
* *(deps)* Update sentry-javascript monorepo to v7.58.1
* *(deps)* Update sentry-javascript monorepo to v7.59.1
* *(deps)* Update sentry-javascript monorepo to v7.59.2
* *(deps)* Update sentry-javascript monorepo to v7.59.3
* *(deps)* Update sentry-javascript monorepo to v7.60.0
* *(deps)* Update sentry-javascript monorepo to v7.73.0
* *(deps)* Update sentry-javascript monorepo to v7.74.0 (#3772)
* *(deps)* Update sentry-javascript monorepo to v7.74.1 (#3778)
* *(deps)* Update sentry-javascript monorepo to v7.75.1 (#3798)
* *(deps)* Update sentry-javascript monorepo to v7.77.0 (#3805)
* *(deps)* Update sentry-javascript monorepo to v7.80.1 (#3819)
* *(deps)* Update sentry-javascript monorepo to v7.85.0 (#3831)
* *(deps)* Update sentry-javascript monorepo to v7.88.0
* *(deps)* Update sub-dependencies
* *(deps)* Update tiptap to v2.1.12 (#3790)
* *(deps)* Update tiptap to v2.1.13 (#3840)
* *(deps)* Update vueuse to v10.5.0 (#3762)
* *(deps)* Update vueuse to v10.6.1 (#3822)
* *(deps)* Update vueuse to v10.7.0 (#3844)
### Features
* *(api tokens)* Add basic api token overview
* *(api tokens)* Add deleting api tokens
* *(api tokens)* Add token creation form
* *(api tokens)* Allow custom selection of expiry dates
* *(api tokens)* Allow selecting all permissions
* *(api tokens)* Format permissions and groups human-readable
* *(api tokens)* Show warning if token has expired
* *(api tokens)* Validate title field when creating a new token
* *(assignees)* Improve avatar list consistency
* *(editor)* Add all slash commands
* *(editor)* Add bubble menu
* *(editor)* Add code highlighting
* *(editor)* Add command list example
* *(editor)* Add comment when pressing ctrl enter
* *(editor)* Add placeholder
* *(editor)* Add proper description for all buttons
* *(editor)* Add tests to check rendering of task description
* *(editor)* Add tooltips for everything
* *(editor)* Add uploading an image on save
* *(editor)* Allow passing placeholder down
* *(editor)* Edit mode
* *(editor)* Edit shortcut to set focus into the editor
* *(editor)* Enable table
* *(editor)* Image upload
* *(editor)* Improve overall styling
* *(editor)* Make image upload work via slash command
* *(editor)* Make task list work
* *(editor)* Mark a checkbox item as done when clicking on its text
* *(editor)* Move all editor related components into one folder
* *(editor)* Only load attachment images when rendering is done
* *(editor)* Open links when clicking on them
* *(editor)* Properly bubble changes when they are made
* *(editor)* Resolve and load attachment images from content
* *(editor)* Save when pressing ctrl enter
* *(gantt)* Implement dynamic sizing on small date ranges (#3750)
* *(i18n)* Add Slovene language for selection in the ui
* *(i18n)* Add arabic to list of selectable languages
* *(i18n)* Add hungarian translation for selection
* *(i18n)* Run translation update directly
* *(i18n)* Update crowdin sync to use v2 api
* *(i18n)* Update translations only once a day
* *(kanban)* Add icon for bucket collapse
* *(kanban)* Add setting for default bucket
* *(kanban)* Save done bucket with project instead of bucket
* *(labels)* Assign random color when creating labels
* *(list view)* Show subtasks nested
* *(migration)* Proper wording for async migration
* *(notifications)* Add option to mark all as read
* *(quick actions)* Show done tasks last
* *(quick actions)* Show labels as labels and tasks with all of their details
* *(quick actions)* Show task identifier
* *(quick actions)* Show tasks for a label when selecting it
* *(quick add magic)* Allow using the project identifier via quick add magic
* *(task)* Add more tests
* *(task)* Group related task action buttons
* *(task)* Immediately set focus on the task search input when opening the related tasks menu
* *(task)* Move task priority to the front when showing tasks inline
* *(task)* Save currently opened task with control/meta + s
* *(tasks)* Make the whole task in list view clickable
* *(tasks)* Update due date text every minute
* *(webhooks)* Add form validation* Allow custom logo via environment variable (#3685) ([cade3df](cade3df3e9a7eca8e0aa9d1553dd5597f0f5a8a2))
* *(webhooks)* Add webhook management form
* Add demo mode warning message ([ed8fb71](ed8fb71ff0b05860f320e2a1fe6c3cb29ed2889a))
* Add setting for default bucket ([04ba101](04ba1011cc3042f657ddb40ee727caf455db8b64))
* Api tokens ([28f2551](28f2551d87b99c59055a4909195e435dbd9794b6))
* Improve error message for invalid API url ([725fd1a](725fd1ad467fb988810cb23f12d372af236bd21d))
* Move from easymde to tiptap editor (#2222) ([26fc9b4](26fc9b4e4f8b96616385f4ca0a77a0ff7ee5eee5))
* Quick actions improvments ([47d5890](47d589002ccef5047a25ea3ad8ebe582c3b0bbc6))
* Webhooks (#3783) ([5d991e5](5d991e539bb3a249447847c13c92ee35d356b902))
### Miscellaneous Tasks
* *(ci)* Sign drone config
* *(editor)* Add break icon
* *(editor)* Add horizontal line icon
* *(editor)* Cleanup
* *(editor)* Cleanup unused options
* *(editor)* Format
* *(editor)* Make sure all tiptap dependencies are updated as one
* *(editor)* Move checklist to the other lists
* *(editor)* Remove converting markdown
* *(editor)* Remove marked usages
* *(editor)* Remove old editor component
* *(editor)* Remove unused components
* *(editor)* Use typed props definition
* *(filter)* Remove debug log
* *(quick actions)* Format* Provide better error messages when refreshing user info fails ([d535879](d5358793de7fc53795329382222e5f3bafc7fba1))
* Add pr lockdown ([07b1e9a](07b1e9a6b76eb7d92640e00a1dec4294efd2947b))
* Cleanup ([a4a2b95](a4a2b95dc7eaad5fe313884eec0d22d7ae5f85c1))
* Debug ([3cb1e7d](3cb1e7dede659acd19410e0611346e0f582f2ff3))
* Format ([c3f85fc](c3f85fcb1988603a58104552b35101b13e93b06e))
* Improve checking for API url '/' suffix (#121) ([311b1d7](311b1d7594cfd03be4d998f4aead041a8ca63f8c))
* Include version json string in release zip ([c4adcf4](c4adcf4655550214ae795d941eb51878f34cedeb))
* Update flake ([64c90c7](64c90c7fe8a77ded21778a798f18862fe966bd1a))
* Update lockfile ([9f82ec4](9f82ec4162151ba32f329cb8e335eff6b21cebd4))
### Other
* *(other)* [skip ci] Updated translations via Crowdin
## [0.21.0] - 2023-07-07
### Bug Fixes
@ -5910,4 +5526,4 @@ Co-committed-by: renovate <renovatebot@kolaente.de>
* Fixed trying to verify an email when there was none
* Fixed loading tasks when the user was not authenticated
## [0.1] - 2018-09-20
## [0.1] - 2018-09-20

View File

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

View File

@ -4,7 +4,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.22.0-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.21.0-brightgreen.svg)](https://dl.vikunja.io)
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
This is the web frontend for Vikunja, written in Vue.js.
@ -49,4 +49,4 @@ pnpm run build
```shell
pnpm run lint
```
```

1
env.d.ts vendored
View File

@ -25,6 +25,7 @@ interface ImportMetaEnv {
readonly SENTRY_ORG?: string
readonly SENTRY_PROJECT?: string
readonly VITE_WORKBOX_DEBUG?: boolean
readonly VITE_IS_ONLINE: boolean
}

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.14.1",
"packageManager": "pnpm@8.11.0",
"keywords": [
"todo",
"productivity",
@ -49,60 +49,60 @@
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/vue-fontawesome": "3.0.5",
"@github/hotkey": "3.1.0",
"@github/hotkey": "2.3.1",
"@infectoone/vue-ganttastic": "2.2.0",
"@intlify/unplugin-vue-i18n": "2.0.0",
"@kyvg/vue3-notification": "3.1.3",
"@sentry/tracing": "7.93.0",
"@sentry/vue": "7.93.0",
"@tiptap/core": "2.1.15",
"@tiptap/extension-blockquote": "2.1.15",
"@tiptap/extension-bold": "2.1.15",
"@tiptap/extension-bullet-list": "2.1.15",
"@tiptap/extension-code": "2.1.15",
"@tiptap/extension-code-block-lowlight": "2.1.15",
"@tiptap/extension-document": "2.1.15",
"@tiptap/extension-dropcursor": "2.1.15",
"@tiptap/extension-gapcursor": "2.1.15",
"@tiptap/extension-hard-break": "2.1.15",
"@tiptap/extension-heading": "2.1.15",
"@tiptap/extension-history": "2.1.15",
"@tiptap/extension-horizontal-rule": "2.1.15",
"@tiptap/extension-image": "2.1.15",
"@tiptap/extension-italic": "2.1.15",
"@tiptap/extension-link": "2.1.15",
"@tiptap/extension-list-item": "2.1.15",
"@tiptap/extension-ordered-list": "2.1.15",
"@tiptap/extension-paragraph": "2.1.15",
"@tiptap/extension-placeholder": "2.1.15",
"@tiptap/extension-strike": "2.1.15",
"@tiptap/extension-table": "2.1.15",
"@tiptap/extension-table-cell": "2.1.15",
"@tiptap/extension-table-header": "2.1.15",
"@tiptap/extension-table-row": "2.1.15",
"@tiptap/extension-task-item": "2.1.15",
"@tiptap/extension-task-list": "2.1.15",
"@tiptap/extension-text": "2.1.15",
"@tiptap/extension-typography": "2.1.15",
"@tiptap/extension-underline": "2.1.15",
"@tiptap/pm": "2.1.15",
"@tiptap/suggestion": "2.1.15",
"@tiptap/vue-3": "2.1.15",
"@intlify/unplugin-vue-i18n": "1.6.0",
"@kyvg/vue3-notification": "3.0.2",
"@sentry/tracing": "7.85.0",
"@sentry/vue": "7.85.0",
"@tiptap/core": "2.1.13",
"@tiptap/extension-blockquote": "2.1.13",
"@tiptap/extension-bold": "2.1.13",
"@tiptap/extension-bullet-list": "2.1.13",
"@tiptap/extension-code": "2.1.13",
"@tiptap/extension-code-block-lowlight": "2.1.13",
"@tiptap/extension-document": "2.1.13",
"@tiptap/extension-dropcursor": "2.1.13",
"@tiptap/extension-gapcursor": "2.1.13",
"@tiptap/extension-hard-break": "2.1.13",
"@tiptap/extension-heading": "2.1.13",
"@tiptap/extension-history": "2.1.13",
"@tiptap/extension-horizontal-rule": "2.1.13",
"@tiptap/extension-image": "2.1.13",
"@tiptap/extension-italic": "2.1.13",
"@tiptap/extension-link": "2.1.13",
"@tiptap/extension-list-item": "2.1.13",
"@tiptap/extension-ordered-list": "2.1.13",
"@tiptap/extension-paragraph": "2.1.13",
"@tiptap/extension-placeholder": "2.1.13",
"@tiptap/extension-strike": "2.1.13",
"@tiptap/extension-table": "2.1.13",
"@tiptap/extension-table-cell": "2.1.13",
"@tiptap/extension-table-header": "2.1.13",
"@tiptap/extension-table-row": "2.1.13",
"@tiptap/extension-task-item": "2.1.13",
"@tiptap/extension-task-list": "2.1.13",
"@tiptap/extension-text": "2.1.13",
"@tiptap/extension-typography": "2.1.13",
"@tiptap/extension-underline": "2.1.13",
"@tiptap/pm": "2.1.13",
"@tiptap/suggestion": "2.1.13",
"@tiptap/vue-3": "2.1.13",
"@types/is-touch-device": "1.0.2",
"@types/lodash.clonedeep": "4.5.9",
"@vueuse/core": "10.7.1",
"@vueuse/router": "10.7.1",
"axios": "1.6.5",
"@vueuse/core": "10.7.0",
"@vueuse/router": "10.7.0",
"axios": "1.6.2",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"date-fns": "3.2.0",
"date-fns": "2.30.0",
"dayjs": "1.11.10",
"dompurify": "3.0.8",
"dompurify": "3.0.6",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
"floating-vue": "5.0.3",
"floating-vue": "2.0.0-beta.24",
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
@ -113,22 +113,22 @@
"sortablejs": "1.15.1",
"tippy.js": "6.3.7",
"ufo": "1.3.2",
"vue": "3.4.8",
"vue": "3.3.10",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.3",
"vue-i18n": "9.9.0",
"vue-i18n": "9.8.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.7",
"@cypress/vite-dev-server": "5.0.6",
"@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",
"@rushstack/eslint-patch": "1.6.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.8",
"@types/node": "20.10.3",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.7",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@typescript-eslint/eslint-plugin": "6.13.2",
"@typescript-eslint/parser": "6.13.2",
"@vitejs/plugin-legacy": "5.2.0",
"@vitejs/plugin-vue": "4.6.2",
"@vitejs/plugin-vue": "4.5.1",
"@vue/eslint-config-typescript": "12.0.0",
"@vue/test-utils": "2.4.3",
"@vue/tsconfig": "0.5.1",
"@vue/tsconfig": "0.4.0",
"autoprefixer": "10.4.16",
"browserslist": "4.22.2",
"caniuse-lite": "1.0.30001576",
"css-has-pseudo": "6.0.1",
"csstype": "3.1.3",
"cypress": "13.6.2",
"esbuild": "0.19.11",
"eslint": "8.56.0",
"caniuse-lite": "1.0.30001566",
"css-has-pseudo": "6.0.0",
"csstype": "3.1.2",
"cypress": "13.6.1",
"esbuild": "0.19.8",
"eslint": "8.55.0",
"eslint-plugin-vue": "9.19.2",
"happy-dom": "12.10.3",
"histoire": "0.17.6",
"postcss": "8.4.33",
"postcss": "8.4.32",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.1",
"postcss-focus-within": "8.0.0",
"postcss-preset-env": "9.3.0",
"rollup": "4.9.4",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.69.7",
"rollup": "4.6.1",
"rollup-plugin-visualizer": "5.10.0",
"sass": "1.69.5",
"start-server-and-test": "2.0.3",
"typescript": "5.3.3",
"vite": "5.0.11",
"typescript": "5.3.2",
"vite": "5.0.6",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.17.4",
"vite-plugin-pwa": "0.17.3",
"vite-plugin-sentry": "1.3.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.1.3",
"vue-tsc": "1.8.27",
"vitest": "1.0.1",
"vue-tsc": "1.8.25",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
},

File diff suppressed because it is too large Load Diff

View File

@ -81,8 +81,9 @@ 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 { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
import {useAuthStore} from '@/stores/auth'
const authStore = useAuthStore()
const {t} = useI18n({useScope: 'global'})
const emit = defineEmits(['update:modelValue'])
@ -92,6 +93,8 @@ 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,
@ -99,7 +102,9 @@ const flatPickerConfig = computed(() => ({
enableTime: false,
wrap: true,
mode: 'range',
locale: getFlatpickrLanguage(),
locale: {
firstDayOf7Days: weekStart.value,
},
}))
const showHowItWorks = ref(false)

View File

@ -47,6 +47,7 @@
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton>
<ProjectSettingsDropdown
v-if="project.id > 0"
class="menu-list-dropdown"
:project="project"
:level="level"
@ -57,6 +58,7 @@
</BaseButton>
</template>
</ProjectSettingsDropdown>
<span class="list-setting-spacer" v-else></span>
</div>
<ProjectsNavigation
v-if="canNestDeeper && childProjectsOpen && canCollapse"

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,6 +105,8 @@ watch(
{immediate: true},
)
const authStore = useAuthStore()
const weekStart = computed(() => authStore.settings.weekStart)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
@ -112,7 +114,9 @@ const flatPickerConfig = computed(() => ({
enableTime: true,
time_24hr: true,
inline: true,
locale: getFlatpickrLanguage(),
locale: {
firstDayOfWeek: weekStart.value,
},
}))
// Since flatpickr dates are strings, we need to convert them to native date objects.
@ -124,10 +128,6 @@ const flatPickrDate = computed({
return
}
const oldDate = formatDate(date.value, 'yyy-LL-dd H:mm')
if (oldDate !== newValue) {
return
}
date.value = createDateFromString(newValue)
updateData()
},
@ -155,6 +155,10 @@ 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)
@ -162,6 +166,7 @@ function setDate(dateString: string) {
newDate.setMinutes(0)
newDate.setSeconds(0)
date.value = newDate
flatPickrDate.value = newDate
updateData()
}

View File

@ -124,7 +124,6 @@ function to(n, index) {
switch (n.name) {
case names.TASK_COMMENT:
case names.TASK_ASSIGNED:
case names.TASK_REMINDER:
to.name = 'task.detail'
to.params.id = n.notification.task.id
break
@ -222,7 +221,7 @@ async function markAllRead() {
height: .35rem;
background: var(--primary);
border-radius: 100%;
margin: 0 .5rem;
margin-left: .5rem;
&.read {
background: transparent;

View File

@ -61,8 +61,6 @@ 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,
@ -83,8 +81,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

@ -149,15 +149,13 @@ async function addTask() {
await Promise.all(newTasks)
const taskRelationService = new TaskRelationService()
const allParentTasks = tasksToCreate.filter(t => t.parent !== null).map(t => t.parent)
const relations = tasksToCreate.map(async t => {
const createdTask = createdTasks[t.title]
if (typeof createdTask === 'undefined') {
return
}
const isParent = allParentTasks.includes(t.title)
if (t.parent === null && !isParent) {
if (t.parent === null) {
emit('taskAdded', createdTask)
return
}
@ -173,19 +171,10 @@ async function addTask() {
relationKind: RELATION_KIND.PARENTTASK,
}))
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [{
...createdParentTask,
relatedTasks: {}, // To avoid endless references
}]
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [createdParentTask]
// we're only emitting here so that the relation shows up in the project
emit('taskAdded', createdTask)
createdParentTask.relatedTasks[RELATION_KIND.SUBTASK] = [{
...createdTask,
relatedTasks: {}, // To avoid endless references
}]
emit('taskAdded', createdParentTask)
return rel
})
await Promise.all(relations)

View File

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

View File

@ -5,7 +5,6 @@
'is-loading': loadingInternal || loading,
'draggable': !(loadingInternal || loading),
'has-light-text': color !== TASK_DEFAULT_COLOR && !colorIsDark(color),
'has-custom-background-color': color !== TASK_DEFAULT_COLOR ? color : undefined,
}"
:style="{'background-color': color !== TASK_DEFAULT_COLOR ? color : undefined}"
@click.exact="openTaskDetail()"
@ -49,10 +48,7 @@
</progress>
<div class="footer">
<labels :labels="task.labels"/>
<priority-label
:priority="task.priority"
:done="task.done"
class="is-inline-flex is-align-items-center"/>
<priority-label :priority="task.priority" :done="task.done" class="is-inline-flex is-align-items-center"/>
<assignee-list
v-if="task.assignees.length > 0"
:assignees="task.assignees"
@ -118,7 +114,7 @@ async function toggleTaskDone(task: ITask) {
...task,
done: !task.done,
})
if (updatedTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
@ -284,16 +280,6 @@ $task-background: var(--white);
width: auto;
}
&.has-custom-background-color {
color: hsl(215, 27.9%, 16.9%); // copied from grey-800 to avoid different values in dark mode
.footer .icon,
.due-date,
.priority-label {
background: hsl(220, 13%, 91%);
}
}
&.has-light-text {
color: var(--white);

View File

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

@ -1,11 +1,16 @@
import {parseURL} from 'ufo'
import {createRandomID} from '@/helpers/randomId'
import type {IProvider} from '@/types/IProvider'
export const redirectToProvider = (provider: IProvider) => {
export const redirectToProvider = (provider: IProvider, redirectUrl = '') => {
// We're not using the redirect url provided by the server to allow redirects when using the electron app.
// The implications are not quite clear yet hence the logic to pass in another redirect url still exists.
const redirectUrl = `${window.location.href.replace('/login', '')}/auth/openid/`
if (redirectUrl === '') {
const {host, protocol} = parseURL(window.location.href)
redirectUrl = `${protocol}//${host}/auth/openid/`
}
const state = createRandomID(24)
localStorage.setItem('state', state)
@ -13,7 +18,7 @@ export const redirectToProvider = (provider: IProvider) => {
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}${provider.key}&response_type=code&scope=openid email profile&state=${state}`
}
export const redirectToProviderOnLogout = (provider: IProvider) => {
if (provider.logoutUrl.length > 0) {
if (provider.logoutUrl.length > 0){
window.location.href = `${provider.logoutUrl}`
}
}

View File

@ -15,32 +15,34 @@ 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, 'gi')) !== null
return text.match(new RegExp('(^| )' + dateExpr, 'g')) !== null
}
export const parseDate = (text: string, now: Date = new Date()): dateParseResult => {
if (matchesDateExpr(text, 'today')) {
const lowerText: string = text.toLowerCase()
if (matchesDateExpr(lowerText, 'today')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today')
}
if (matchesDateExpr(text, 'tomorrow')) {
if (matchesDateExpr(lowerText, 'tomorrow')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('tomorrow')), 'tomorrow')
}
if (matchesDateExpr(text, 'next monday')) {
if (matchesDateExpr(lowerText, 'next monday')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextMonday')), 'next monday')
}
if (matchesDateExpr(text, 'this weekend')) {
if (matchesDateExpr(lowerText, 'this weekend')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('thisWeekend')), 'this weekend')
}
if (matchesDateExpr(text, 'later this week')) {
if (matchesDateExpr(lowerText, 'later this week')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterThisWeek')), 'later this week')
}
if (matchesDateExpr(text, 'later next week')) {
if (matchesDateExpr(lowerText, 'later next week')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('laterNextWeek')), 'later next week')
}
if (matchesDateExpr(text, 'next week')) {
if (matchesDateExpr(lowerText, 'next week')) {
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('nextWeek')), 'next week')
}
if (matchesDateExpr(text, 'next month')) {
if (matchesDateExpr(lowerText, 'next month')) {
const date: Date = new Date()
date.setDate(1)
date.setMonth(date.getMonth() + 1)
@ -50,7 +52,7 @@ export const parseDate = (text: string, now: Date = new Date()): dateParseResult
return addTimeToDate(text, date, 'next month')
}
if (matchesDateExpr(text, 'end of month')) {
if (matchesDateExpr(lowerText, 'end of month')) {
const curDate: Date = new Date()
const date: Date = new Date(curDate.getFullYear(), curDate.getMonth() + 1, 0)
date.setHours(calculateNearestHours(date))
@ -68,7 +70,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(month.newText, month.date, parsed.foundText)
return addTimeToDate(text, month.date, parsed.foundText)
}
parsed = getDateFromTextIn(text, now)
@ -121,7 +123,7 @@ const addTimeToDate = (text: string, date: Date, previousMatch: string | null):
const replace = results !== null ? results[0] : previousMatch
return {
newText: replaceAll(text, replace, '').trim(),
newText: replaceAll(text, replace, ''),
date: date,
}
}

View File

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

View File

@ -160,7 +160,6 @@
"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,7 +160,6 @@
"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,7 +160,6 @@
"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,7 +160,6 @@
"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?",
@ -178,7 +177,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": "Klicks zum Fortfahren auf den Button unten. 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.",
"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.",
@ -186,7 +185,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": "Um die Löschung deines Accounts abzubrechen, klicke unten auf den Button:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Löschung meines Accounts abbrechen",
"scheduledCancelSuccess": "Wir werden deinen Account nicht löschen."
},
@ -938,7 +937,7 @@
"delete": "Diese Aufgabe löschen",
"priority": "Die Priorität dieser Aufgabe ändern",
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen",
"save": "Aktuelle Aufgabe speichern"
"save": "Save the current task"
},
"project": {
"title": "Projektansichten",

View File

@ -160,7 +160,6 @@
"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?",
@ -178,7 +177,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": "Klicks zum Fortfahren auf den Button unten. 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.",
"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.",
@ -186,7 +185,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": "Um die Löschung deines Accounts abzubrechen, klicke unten auf den Button:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Löschung meines Accounts abbrechen",
"scheduledCancelSuccess": "Wir werden deinen Account nicht löschen."
},
@ -938,7 +937,7 @@
"delete": "Diese Aufgabe löschen",
"priority": "Die Priorität dieser Aufgabe ändern",
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen",
"save": "Aktuelle Aufgabe speichern"
"save": "Save the current task"
},
"project": {
"title": "Projektansichten",

View File

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

View File

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

View File

@ -160,7 +160,6 @@
"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

@ -9,7 +9,6 @@ export const NOTIFICATION_NAMES = {
'TASK_COMMENT': 'task.comment',
'TASK_ASSIGNED': 'task.assigned',
'TASK_DELETED': 'task.deleted',
'TASK_REMINDER': 'task.reminder',
'PROJECT_CREATED': 'project.created',
'TEAM_MEMBER_ADDED': 'team.member.added',
} as const
@ -36,11 +35,6 @@ interface NotificationCreated extends Notification {
project: IProject
}
interface NotificationTaskReminder extends Notification {
task: ITask
project: IProject
}
interface NotificationMemberAdded extends Notification {
member: IUser
team: ITeam
@ -49,7 +43,7 @@ interface NotificationMemberAdded extends Notification {
export interface INotification extends IAbstract {
id: number
name: string
notification: NotificationTask | NotificationAssigned | NotificationDeleted | NotificationCreated | NotificationMemberAdded | NotificationTaskReminder
notification: NotificationTask | NotificationAssigned | NotificationDeleted | NotificationCreated | NotificationMemberAdded
read: boolean
readAt: Date | null

View File

@ -56,12 +56,6 @@ export default class NotificationModel extends AbstractModel<INotification> impl
team: new TeamModel(this.notification.team),
}
break
case NOTIFICATION_NAMES.TASK_REMINDER:
this.notification = {
task: new TaskModel(this.notification.task),
project: new ProjectModel(this.notification.project),
}
break
}
this.created = new Date(this.created)
@ -94,8 +88,6 @@ export default class NotificationModel extends AbstractModel<INotification> impl
}
return `added ${who} to the ${this.notification.team.name} team`
case NOTIFICATION_NAMES.TASK_REMINDER:
return `Reminder for ${this.notification.task.getTextIdentifier()} ${this.notification.task.title} (${this.notification.project.title})`
}
return ''

View File

@ -112,16 +112,6 @@ 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')
@ -451,7 +441,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',
@ -459,52 +449,39 @@ 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, foundText} = getDateFromText(`Lorem Ipsum ${c}`, now)
const {date} = 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, foundText} = getDateFromText(`${c} Lorem Ipsum`, now)
const {date} = 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)
})
}
})
@ -555,20 +532,6 @@ 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

@ -27,7 +27,7 @@ function redirectToProviderIfNothingElseIsEnabled() {
(window.location.pathname.startsWith('/login') || window.location.pathname === '/') && // Kinda hacky, but prevents an endless loop.
window.location.search.includes('redirectToProvider=true')
) {
redirectToProvider(auth.openidConnect.providers[0])
redirectToProvider(auth.openidConnect.providers[0], auth.openidConnect.redirectUrl)
}
}

View File

@ -11,6 +11,7 @@ const workboxVersion = 'v7.0.0'
importScripts(`${fullBaseUrl}workbox-${workboxVersion}/workbox-sw.js`)
workbox.setConfig({
modulePathPrefix: `${fullBaseUrl}workbox-${workboxVersion}`,
debug: Boolean(import.meta.env.VITE_WORKBOX_DEBUG),
})
import { precacheAndRoute } from 'workbox-precaching'

View File

@ -54,7 +54,7 @@ import {useI18n} from 'vue-i18n'
import type {RouteLocationNormalized} from 'vue-router'
import {useBaseStore} from '@/stores/base'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
import {useAuthStore} from '@/stores/auth'
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
@ -126,13 +126,16 @@ 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: getFlatpickrLanguage(),
locale: {
firstDayOfWeek: authStore.settings.weekStart,
},
}))
</script>

View File

@ -175,11 +175,7 @@ const tasks = ref<ITask[]>([])
watch(
allTasks,
() => {
tasks.value = [...allTasks.value]
if (projectId < 0) {
return
}
tasks.value = tasks.value.filter(t => typeof t.relatedTasks?.parenttask === 'undefined')
tasks.value = [...allTasks.value].filter(t => typeof t.relatedTasks?.parenttask === 'undefined')
},
)
@ -245,9 +241,9 @@ function updateTaskList(task: ITask) {
loadTasks()
}
else {
allTasks.value = [
tasks.value = [
task,
...allTasks.value,
...tasks.value,
]
}

View File

@ -10,38 +10,38 @@ 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[]>([])
const tokens = ref([])
const apiDocsUrl = window.API_URL + '/docs'
const showCreateForm = ref(false)
const availableRoutes = ref(null)
const newToken = ref<IApiToken>(new ApiTokenModel())
const newToken = ref(new ApiTokenModel())
const newTokenExpiry = ref<string | number>(30)
const newTokenExpiryCustom = ref(new Date())
const newTokenPermissions = ref({})
const newTokenPermissionsGroup = ref({})
const newTokenTitleValid = ref(true)
const apiTokenTitle = ref()
const tokenCreatedSuccessMessage = ref('')
const showDeleteModal = ref<boolean>(false)
const tokenToDelete = ref<IApiToken>()
const showDeleteModal = ref(false)
const tokenToDelete = ref(null)
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: getFlatpickrLanguage(),
locale: {
firstDayOfWeek: authStore.settings.weekStart,
},
minDate: now,
}))
@ -65,8 +65,8 @@ function resetPermissions() {
async function deleteToken() {
await service.delete(tokenToDelete.value)
showDeleteModal.value = false
const index = tokens.value.findIndex(el => el.id === tokenToDelete.value.id)
tokenToDelete.value = null
const index = tokens.value.findIndex(el => el.id === tokenToDelete.value.id)
if (index === -1) {
return
}
@ -111,32 +111,6 @@ async function createToken() {
function formatPermissionTitle(title: string): string {
return title.replaceAll('_', ' ')
}
function selectPermissionGroup(group: string, checked: boolean) {
Object.entries(availableRoutes.value[group]).forEach(entry => {
const [key] = entry
newTokenPermissions.value[group][key] = checked
})
}
function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
if (checked) {
// Check if all permissions of that group are checked and check the "select all" checkbox in that case
let allChecked = true
Object.entries(availableRoutes.value[group]).forEach(entry => {
const [key] = entry
if (!newTokenPermissions.value[group][key]) {
allChecked = false
}
})
if (allChecked) {
newTokenPermissionsGroup.value[group] = true
}
} else {
newTokenPermissionsGroup.value[group] = false
}
}
</script>
<template>
@ -241,31 +215,15 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
<p>{{ $t('user.settings.apiTokens.permissionExplanation') }}</p>
<div v-for="(routes, group) in availableRoutes" class="mb-2" :key="group">
<strong class="is-capitalized">{{ formatPermissionTitle(group) }}</strong><br/>
<template
v-if="Object.keys(routes).length > 1"
>
<fancycheckbox
class="mr-2 is-italic"
v-model="newTokenPermissionsGroup[group]"
@update:model-value="checked => selectPermissionGroup(group, checked)"
>
{{ $t('user.settings.apiTokens.selectAll') }}
</fancycheckbox>
<br/>
</template>
<template
<fancycheckbox
v-for="(paths, route) in routes"
:key="group+'-'+route"
class="mr-2 is-capitalized"
v-model="newTokenPermissions[group][route]"
>
<fancycheckbox
class="mr-2 is-capitalized"
v-model="newTokenPermissions[group][route]"
@update:model-value="checked => toggleGroupPermissionsFromChild(group, checked)"
>
{{ formatPermissionTitle(route) }}
</fancycheckbox>
<br/>
</template>
{{ formatPermissionTitle(route) }}
</fancycheckbox>
<br/>
</div>
</div>