Compare commits

...

346 Commits

Author SHA1 Message Date
Sean Hurley a94023268c Ignore trying to tell if filters are set since it doesn't work for saved lists 2023-11-17 22:45:35 -07:00
Sean Hurley 88e583f786 Only reload tasks when filtering on due date and due date is set on the updated task 2023-11-17 22:45:35 -07:00
Sean Hurley 5e51b144c2 Refresh tasks lists after a task has been updated 2023-11-17 22:45:35 -07:00
renovate 6711a08de9 fix(deps): update sentry-javascript monorepo to v7.80.1 (#3819)
Reviewed-on: vikunja/frontend#3819
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:50:00 +00:00
renovate 7fe33c6662 fix(deps): update dependency @types/sortablejs to v1.15.5 (#3818)
Reviewed-on: vikunja/frontend#3818
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:49:51 +00:00
renovate e61b215dc1 fix(deps): update dependency ufo to v1.3.2 (#3824)
Reviewed-on: vikunja/frontend#3824
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:03:25 +00:00
renovate 3b5cb1ade3 fix(deps): update dependency vue-i18n to v9.7.0 (#3825)
Reviewed-on: vikunja/frontend#3825
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 22:01:43 +00:00
renovate 89e28cbdf2 chore(deps): update dev-dependencies (#3826)
Reviewed-on: vikunja/frontend#3826
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-17 21:59:59 +00:00
renovate e9e836f068 chore(deps): update pnpm to v8.10.5 2023-11-17 21:40:42 +00:00
kolaente aa5e11915e
fix(filter): don't prevent entering date math strings
Resolves https://community.vikunja.io/t/filter-setting-s/1791/2
2023-11-17 19:38:55 +01:00
kolaente 7f279c98e1
fix(tasks): don't use the filter for upcoming when one is set for the home page
Resolves https://github.com/go-vikunja/frontend/issues/132
2023-11-17 19:08:08 +01:00
kolaente 3c1861eb6a
fix(settings): move overdue remindeer time below 2023-11-17 19:03:58 +01:00
kolaente 75262b716f
fix(kanban): opening a task from the kanban board and then reloading the page should not crash everything when then navigating back
Before this fix, the following would not work:

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

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

Co-authored-by: Sean Hurley <sean.hurley6@gmail.com>
Reviewed-on: vikunja/frontend#3061
Reviewed-by: konrad <k@knt.li>
Co-authored-by: ThatHurleyGuy <sean@hurley.io>
Co-committed-by: ThatHurleyGuy <sean@hurley.io>
2023-11-15 12:10:18 +00:00
kolaente 3f42ce2b34
fix(filter): make other filters are not available for project selection 2023-11-15 12:47:19 +01:00
renovate 8b8da40265 chore(deps): update dev-dependencies (#3821)
Reviewed-on: vikunja/frontend#3821
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-15 11:23:57 +00:00
Frederick [Bot] 0f23cc2162 [skip ci] Updated translations via Crowdin 2023-11-14 00:13:32 +00:00
Frederick [Bot] adf80d9184 [skip ci] Updated translations via Crowdin 2023-11-11 00:13:51 +00:00
Frederick [Bot] e3dfcafc29 [skip ci] Updated translations via Crowdin 2023-11-10 00:13:52 +00:00
Frederick [Bot] a9df58109f [skip ci] Updated translations via Crowdin 2023-11-09 00:14:14 +00:00
kolaente 59a7360608
feat(migration): proper wording for async migration 2023-11-09 00:14:37 +01:00
renovate 29e128c64c chore(deps): update dev-dependencies (#3813)
Reviewed-on: vikunja/frontend#3813
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 19:24:57 +00:00
renovate cec50d912c fix(deps): update dependency vue to v3.3.8 (#3814)
Reviewed-on: vikunja/frontend#3814
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 19:24:39 +00:00
renovate 53564ec46c fix(deps): update dependency @types/lodash.clonedeep to v4.5.9 (#3817)
Reviewed-on: vikunja/frontend#3817
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 17:07:48 +00:00
renovate e9cd7aac69 fix(deps): update dependency @intlify/unplugin-vue-i18n to v1.5.0 (#3812)
Reviewed-on: vikunja/frontend#3812
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 17:06:56 +00:00
renovate a47bfb3ff1 fix(deps): update dependency @types/is-touch-device to v1.0.2 (#3816)
Reviewed-on: vikunja/frontend#3816
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 15:06:23 +00:00
renovate 86eb4da2e3 fix(deps): update dependency @fortawesome/vue-fontawesome to v3.0.5 (#3815)
Reviewed-on: vikunja/frontend#3815
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 15:03:14 +00:00
renovate d1882e9c3f fix(deps): update dependency vue-i18n to v9.6.5 (#3807)
Reviewed-on: vikunja/frontend#3807
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 14:55:42 +00:00
renovate 974755ffc2 fix(deps): update sentry-javascript monorepo to v7.77.0 (#3805)
Reviewed-on: vikunja/frontend#3805
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-07 14:32:45 +00:00
Frederick [Bot] f00d49cada [skip ci] Updated translations via Crowdin 2023-11-06 00:04:12 +00:00
Frederick [Bot] e41ec4e8b2 [skip ci] Updated translations via Crowdin 2023-11-04 00:22:49 +00:00
kolaente 218d72494a
fix: lint 2023-11-03 12:39:02 +01:00
kolaente bde212d432
fix(editor): change description when switching between tasks 2023-11-03 12:36:20 +01:00
kolaente 64a8dd189b
fix(editor): always set mode to preview after save 2023-11-03 12:27:21 +01:00
kolaente ba766a29af
fix(editor): check for empty content 2023-11-03 12:22:38 +01:00
renovate e02a106c64 chore(deps): update dev-dependencies (#3811)
Reviewed-on: vikunja/frontend#3811
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-03 11:06:43 +00:00
renovate ccdc5d4868 fix(deps): update dependency @github/hotkey to v2.3.0 (#3810)
Reviewed-on: vikunja/frontend#3810
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-11-03 11:04:11 +00:00
renovate 9240739a4b chore(deps): update pnpm to v8.10.2 2023-11-01 17:10:19 +00:00
renovate 963d91c4d5 chore(deps): update dev-dependencies (#3806)
Reviewed-on: vikunja/frontend#3806
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-31 12:14:10 +00:00
renovate f33d154b37 fix(deps): update dependency @github/hotkey to v2.2.0 (#3809)
Reviewed-on: vikunja/frontend#3809
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-31 11:42:47 +00:00
Frederick [Bot] b12db63de0 [skip ci] Updated translations via Crowdin 2023-10-31 00:12:53 +00:00
Frederick [Bot] a3e729a3c8 [skip ci] Updated translations via Crowdin 2023-10-30 00:04:31 +00:00
kolaente 8e0ba555ed
fix(editor): check for almost empty editor value 2023-10-29 19:42:20 +01:00
kolaente 9cf81e1478
fix(editor): use modelValue directly to update values in the editor 2023-10-29 19:39:38 +01:00
renovate 4350d78178 fix(deps): update dependency vue-i18n to v9.6.1 (#3803)
Reviewed-on: vikunja/frontend#3803
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-27 09:10:07 +00:00
renovate cea27bb754 chore(deps): update dev-dependencies (#3802)
Reviewed-on: vikunja/frontend#3802
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-27 08:35:43 +00:00
renovate 4b7f8c265d fix(deps): update dependency axios to v1.6.0 (#3801)
Reviewed-on: vikunja/frontend#3801
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-27 08:35:05 +00:00
renovate 412e6e77b4 chore(deps): update dependency @types/node to v20 (#3796)
Reviewed-on: vikunja/frontend#3796
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-26 21:21:54 +00:00
renovate 45abdda680 fix(deps): update dependency vue-i18n to v9.6.0 (#3800)
Reviewed-on: vikunja/frontend#3800
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-26 21:21:20 +00:00
renovate 0b2188d72d fix(deps): update sentry-javascript monorepo to v7.75.1 (#3798)
Reviewed-on: vikunja/frontend#3798
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-26 21:20:56 +00:00
renovate 143a2a105d fix(deps): update dependency vue to v3.3.7 (#3799)
Reviewed-on: vikunja/frontend#3799
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-26 20:51:14 +00:00
renovate 68d18934d8 chore(deps): update dev-dependencies (#3793)
Reviewed-on: vikunja/frontend#3793
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-26 11:24:54 +00:00
renovate cea3274a90 chore(deps): update dependency node (#3797)
Reviewed-on: vikunja/frontend#3797
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-26 10:55:57 +00:00
Frederick [Bot] 72f57a220d [skip ci] Updated translations via Crowdin 2023-10-24 00:04:26 +00:00
Frederick [Bot] b94acfcc84 [skip ci] Updated translations via Crowdin 2023-10-23 00:04:16 +00:00
kolaente 5f2787e18d
fix(task): use editor as preview first, then check for edit 2023-10-22 22:47:52 +02:00
kolaente 2eac17ed57
fix(editor): commands list in dark mode 2023-10-22 19:58:25 +02:00
renovate 8d566c9371 fix(deps): update dependency @types/is-touch-device to v1.0.1 (#3786)
Reviewed-on: vikunja/frontend#3786
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-22 17:51:25 +00:00
renovate ab5118b51b chore(deps): update pnpm to v8.9.2 2023-10-22 17:42:39 +00:00
kolaente 0b294de132 fix(task): make sure the modal close button is not overlapped with the title field (#3256)
Resolves vikunja/frontend#3252
Reviewed-on: vikunja/frontend#3256
Co-authored-by: kolaente <k@knt.li>
Co-committed-by: kolaente <k@knt.li>
2023-10-22 17:40:50 +00:00
renovate c1149273f9 fix(deps): update dependency @types/sortablejs to v1.15.4 (#3788)
Reviewed-on: vikunja/frontend#3788
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-22 17:31:22 +00:00
renovate 7496be5a44 fix(deps): update tiptap to v2.1.12 (#3790)
Reviewed-on: vikunja/frontend#3790
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-22 17:22:46 +00:00
renovate a35b0f64a2 fix(deps): update dependency lowlight to v2.9.0 (#3789)
Reviewed-on: vikunja/frontend#3789
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-22 17:01:34 +00:00
renovate 6c2b30f8ef fix(deps): update dependency @types/lodash.clonedeep to v4.5.8 (#3787)
Reviewed-on: vikunja/frontend#3787
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-22 15:22:23 +00:00
renovate daa720669a fix(deps): update sentry-javascript monorepo to v7.74.1 (#3778)
Reviewed-on: vikunja/frontend#3778
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-22 15:12:47 +00:00
konrad 26fc9b4e4f feat: move from easymde to tiptap editor (#2222)
Reviewed-on: vikunja/frontend#2222
2023-10-22 13:48:57 +00:00
kolaente 37af478811
chore(editor): remove marked usages 2023-10-22 15:18:39 +02:00
kolaente 22223a56bd
chore(editor): remove converting markdown 2023-10-22 15:12:36 +02:00
kolaente c367b70ccc
chore(editor): remove unused components 2023-10-22 15:12:23 +02:00
kolaente 9103ad8505
chore(deps): remove unused dependencies 2023-10-22 14:46:21 +02:00
kolaente e4eaca82e1
fix(editor): add missing dependency 2023-10-22 14:29:48 +02:00
kolaente 229beec1d1
fix(editor): lint 2023-10-22 14:24:10 +02:00
kolaente 803f9c81c2
fix(editor): make tests work with changed structure 2023-10-22 14:21:28 +02:00
kolaente c6b123734b
feat(editor): add tests to check rendering of task description 2023-10-22 14:21:13 +02:00
kolaente 0154b2a475
fix(editor): allow checking a checkbox even when the editor is set to read only 2023-10-22 13:26:01 +02:00
kolaente 32ca8853bc
fix(editor): don't use global shortcut when anything is focused 2023-10-22 12:48:31 +02:00
kolaente f6d5cbcf6f
feat(editor): only load attachment images when rendering is done 2023-10-22 12:38:34 +02:00
kolaente d7503dc4a2
feat(editor): edit mode 2023-10-22 12:08:27 +02:00
kolaente 632e3c5a0b
fix(editor): duplicate name for extension 2023-10-22 11:02:03 +02:00
kolaente c61f1a45fb
fix(editor): placeholder showing or not showing 2023-10-22 11:00:42 +02:00
kolaente 2f3196ef86
fix(editor): duplicate name 2023-10-22 10:50:16 +02:00
kolaente 2864854cd4
fix(editor): don't prevent typing editor focus shortcut when other instance of an editor is focused already 2023-10-22 10:47:13 +02:00
kolaente a453449fea
fix(editor): reset on empty 2023-10-22 10:41:34 +02:00
kolaente abb6630b4b
feat(editor): add comment when pressing ctrl enter 2023-10-22 10:40:12 +02:00
kolaente 63c40b29b0
feat(editor): save when pressing ctrl enter 2023-10-22 10:38:53 +02:00
Frederick [Bot] b7ff71ba76 [skip ci] Updated translations via Crowdin 2023-10-22 00:03:33 +00:00
kolaente 19a78f1f75
fix(editor): lint 2023-10-21 19:52:56 +02:00
kolaente d6a41fa518
chore(editor): remove old editor component 2023-10-21 19:48:17 +02:00
kolaente 859fc1e94e
feat(editor): edit shortcut to set focus into the editor 2023-10-21 19:46:46 +02:00
kolaente aa715dd9e1
chore(editor): cleanup unused options 2023-10-21 19:46:25 +02:00
kolaente daa2ed3b1c
feat(editor): allow passing placeholder down 2023-10-21 19:33:32 +02:00
kolaente 1443e23f18
chore(editor): cleanup 2023-10-21 19:29:27 +02:00
kolaente 34420b623c
fix(editor): use edit enable 2023-10-21 19:29:19 +02:00
kolaente 80dc35eabb
fix(editor): actions styling 2023-10-21 19:29:00 +02:00
kolaente cb1d2b3834
fix(editor): always show placeholder when empty 2023-10-21 19:18:28 +02:00
kolaente 0ef775e9b9
feat(editor): improve overall styling 2023-10-21 19:08:11 +02:00
kolaente a7e4e3adf9
feat(editor): add placeholder 2023-10-21 19:02:55 +02:00
kolaente d005875bbf
chore(editor): make sure all tiptap dependencies are updated as one 2023-10-21 18:57:17 +02:00
kolaente dc3ee112bd
chore(editor): cleanup 2023-10-21 18:47:04 +02:00
kolaente 9b20dc1899
feat(editor): properly bubble changes when they are made 2023-10-21 18:40:30 +02:00
kolaente 22103626b8
fix(editor): make checklist indicator work again 2023-10-21 18:18:17 +02:00
kolaente 4f2d7b3ce2
feat(editor): add uploading an image on save 2023-10-21 18:03:59 +02:00
kolaente 76d31c84ad
feat(editor): add tooltips for everything 2023-10-21 17:48:35 +02:00
kolaente 66c37f10e0
chore(editor): cleanup 2023-10-21 14:10:26 +02:00
kolaente 0b2aa723a6
feat(editor): open links when clicking on them 2023-10-21 14:07:39 +02:00
kolaente d75a963d08
feat(editor): add code highlighting 2023-10-21 14:06:47 +02:00
kolaente beefc1d5ef
feat(editor): add bubble menu 2023-10-21 14:02:53 +02:00
kolaente 17c23d9463
feat(editor): make image upload work via slash command 2023-10-21 13:28:59 +02:00
kolaente 02ab1b8c0a
feat(editor): add all slash commands 2023-10-21 13:00:12 +02:00
kolaente e81c98fe5b
chore(editor): format 2023-10-21 11:52:20 +02:00
kolaente 3bf806f00c
fix(editor): add missing dependencies for commands 2023-10-21 11:46:02 +02:00
kolaente aea3f86a8f
feat(editor): add command list example
copied from 252acb32d2/demos/src/Experiments/Commands/Vue
2023-10-21 11:33:49 +02:00
kolaente 5297208d92
feat(editor): move all editor related components into one folder 2023-10-21 11:15:17 +02:00
kolaente c84bcfddba
feat(editor): add proper description for all buttons 2023-10-21 11:10:43 +02:00
kolaente 0772acbead
chore(editor): format 2023-10-21 10:53:41 +02:00
renovate 123c665d9d chore(deps): update dependency eslint to v8.52.0 (#3785)
Reviewed-on: vikunja/frontend#3785
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-21 08:08:26 +00:00
kolaente 4f3efe4454
feat(editor): resolve and load attachment images from content 2023-10-20 23:03:38 +02:00
kolaente 671c658868
fix(editor): actually populate loaded data into the editor 2023-10-20 22:52:21 +02:00
kolaente 05bf7ccf0b
feat(editor): image upload 2023-10-20 22:43:10 +02:00
renovate b76acb15c7 chore(deps): update dev-dependencies (major) (#3741)
Reviewed-on: vikunja/frontend#3741
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-20 19:34:11 +00:00
kolaente 953361c480
chore(deps): update lockfile 2023-10-20 17:10:55 +02:00
kolaente 8b60e5b2c8
fix(editor): add icons for clearing marks and nodes 2023-10-20 17:10:06 +02:00
kolaente faf93a6088
feat(editor): enable table 2023-10-20 17:10:06 +02:00
kolaente 8e07d9647a
chore(editor): move checklist to the other lists 2023-10-20 17:10:06 +02:00
kolaente 08959fdb77
fix(editor): focus state 2023-10-20 17:10:06 +02:00
kolaente e716fd1bf9
fix(editor): list styling 2023-10-20 17:10:06 +02:00
kolaente 63865028b8
feat(editor): make task list work 2023-10-20 17:10:06 +02:00
kolaente e760ce45e4
fix(editor): checklist button icon 2023-10-20 17:10:06 +02:00
kolaente af9eb358ee
fix(editor): permission check for table editing 2023-10-20 17:10:06 +02:00
kolaente ddcf6bf0a5
fix(editor): image button icon 2023-10-20 17:10:06 +02:00
kolaente 9c71e30efe
chore(editor): add break icon 2023-10-20 17:10:06 +02:00
kolaente c58ad47782
chore(editor): use typed props definition 2023-10-20 17:10:05 +02:00
kolaente ca0d9e6bd5
chore(editor): add horizontal line icon 2023-10-20 17:10:05 +02:00
kolaente ad3234b19f
chore(deps): update dependencies 2023-10-20 17:10:05 +02:00
Dominik Pschenitschni 24b8915983
wip: tiptap editor 2023-10-20 17:10:05 +02:00
kolaente 01c2acdf34
chore(deps): update flake 2023-10-20 17:09:55 +02:00
kolaente ff2b4b8bf4
feat(notifications): add option to mark all as read 2023-10-20 16:52:03 +02:00
kolaente d73c62a424
fix(task): correctly build task identifier 2023-10-20 16:52:03 +02:00
renovate cac41a1c86 fix(deps): update dependency vue to v3.3.6 (#3784)
Reviewed-on: vikunja/frontend#3784
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-20 14:01:41 +00:00
kolaente aeed4b3a3b
fix(settings): allow removing the default project via settings 2023-10-20 16:01:22 +02:00
renovate 8992caadf9 fix(deps): update dependency marked to v9.1.2 (#3774)
Reviewed-on: vikunja/frontend#3774
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-20 13:17:59 +00:00
renovate b2b423aee5 chore(deps): update dependency node to v18.18.2 2023-10-20 12:08:04 +00:00
konrad 5d991e539b feat: webhooks (#3783)
Reviewed-on: vikunja/frontend#3783
2023-10-20 11:56:59 +00:00
renovate accde483cb fix(deps): update dependency vue to v3.3.5 (#3782)
Reviewed-on: vikunja/frontend#3782
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-20 11:38:11 +00:00
renovate 2d5e560b74 chore(deps): update dev-dependencies (#3780)
Reviewed-on: vikunja/frontend#3780
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-20 11:37:50 +00:00
kolaente 5d91134b48
fix(tasks): use mousedown event instead of click to close the task popup
Resolves vikunja/frontend#3779
2023-10-20 13:14:46 +02:00
kolaente 0e5415a2c9
fix(webhooks): styling 2023-10-20 12:32:55 +02:00
kolaente 779aad1b2d
feat(webhooks): add form validation 2023-10-20 12:32:46 +02:00
kolaente 3d2fe4cf65
feat(webhooks): add webhook management form 2023-10-18 20:12:48 +02:00
renovate c38421466b chore(deps): update dev-dependencies (#3776)
Reviewed-on: vikunja/frontend#3776
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-18 15:44:47 +00:00
Frederick [Bot] df09bca010 [skip ci] Updated translations via Crowdin 2023-10-18 00:08:50 +00:00
kolaente b9717f504d
feat(i18n): add arabic to list of selectable languages 2023-10-17 18:44:08 +02:00
Frederick [Bot] bb7c4f40a0 [skip ci] Updated translations via Crowdin 2023-10-17 00:09:16 +00:00
Frederick [Bot] 0b9ef50f80 [skip ci] Updated translations via Crowdin 2023-10-16 00:36:16 +00:00
Frederick [Bot] cd295960a4 [skip ci] Updated translations via Crowdin 2023-10-14 00:08:49 +00:00
renovate eb591fdd3c chore(deps): update dev-dependencies (#3769)
Reviewed-on: vikunja/frontend#3769
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-13 13:59:06 +00:00
renovate 23e1899fce fix(deps): update dependency @github/hotkey to v2.1.1 (#3770)
Reviewed-on: vikunja/frontend#3770
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-13 13:32:18 +00:00
renovate 22968ba639 fix(deps): update dependency pinia to v2.1.7 (#3771)
Reviewed-on: vikunja/frontend#3771
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-13 13:17:47 +00:00
renovate b345f0ad61 fix(deps): update sentry-javascript monorepo to v7.74.0 (#3772)
Reviewed-on: vikunja/frontend#3772
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-13 12:27:20 +00:00
Frederick [Bot] 4df34701ab [skip ci] Updated translations via Crowdin 2023-10-13 00:08:52 +00:00
renovate a5f7487bd0 fix(deps): update dependency marked to v9.1.1 (#3768)
Reviewed-on: vikunja/frontend#3768
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-11 21:42:37 +00:00
kolaente ae001c6ca7
fix(user): allow openid users to request their deletion
Resolves https://community.vikunja.io/t/delete-user-not-possible-when-using-oidc/1689/4
2023-10-11 19:07:11 +02:00
kolaente f0b340a9c7
feat(task): save currently opened task with control/meta + s 2023-10-11 17:44:17 +02:00
renovate 40538df392 fix(deps): update dependency @github/hotkey to v2.1.0 (#3766)
Reviewed-on: vikunja/frontend#3766
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-11 09:01:48 +00:00
renovate fc17b16c60 chore(deps): update dependency sass to v1.69.2 (#3767)
Reviewed-on: vikunja/frontend#3767
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-11 07:34:29 +00:00
renovate 6c59b4e2d2 fix(deps): update dependency highlight.js to v11.9.0 (#3763)
Reviewed-on: vikunja/frontend#3763
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-10 19:21:25 +00:00
renovate e8a38ed482 fix(deps): update vueuse to v10.5.0 (#3762)
Reviewed-on: vikunja/frontend#3762
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-10 18:28:10 +00:00
renovate f5604dcac6 chore(deps): update dependency node to v18.18.1 2023-10-10 16:24:22 +00:00
renovate 049c644959 chore(deps): update dev-dependencies (#3761)
Reviewed-on: vikunja/frontend#3761
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-10 16:23:29 +00:00
kolaente 07b1e9a6b7
chore: add pr lockdown 2023-10-10 18:02:40 +02:00
renovate 7aedf6ee1f chore(deps): update pnpm to v8.9.0 2023-10-09 12:09:54 +00:00
renovate bc9bfe3300 fix(deps): update dependency marked to v9.1.0 (#3760)
Reviewed-on: vikunja/frontend#3760
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-05 07:06:20 +00:00
renovate c2005c6c71 chore(deps): update dev-dependencies (#3757)
Reviewed-on: vikunja/frontend#3757
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-04 06:48:23 +00:00
renovate d7cbade64e chore(deps): update node.js to v20.8 (#3756)
Reviewed-on: vikunja/frontend#3756
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-03 14:59:12 +00:00
renovate 06b00b77ed fix(deps): update dependency dompurify to v3.0.6 (#3754)
Reviewed-on: vikunja/frontend#3754
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-03 14:58:26 +00:00
renovate f2392cef7e fix(deps): update dependency vue-router to v4.2.5 (#3755)
Reviewed-on: vikunja/frontend#3755
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-03 14:19:27 +00:00
renovate e6f2b36d88 fix(deps): update dependency dayjs to v1.11.10 (#3753)
Reviewed-on: vikunja/frontend#3753
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-10-03 14:16:01 +00:00
kolaente 7d2fcd26f2 chore(deps): update lockfile 2023-10-03 13:34:11 +00:00
renovate 369e22f224 fix(deps): update dependency ufo to v1.3.1 2023-10-03 13:34:11 +00:00
kolaente 0ff5b90ebd chore(deps): update lockfile 2023-10-03 12:16:07 +00:00
kolaente e89245e42d fix(deps): update dependency @infectoone/vue-ganttastic to v2.2.0 2023-10-03 12:16:07 +00:00
kolaente 35717a1e29 fix(deps): update dependency @kyvg/vue3-notification to v3 2023-10-03 12:16:07 +00:00
kolaente e46cf2fa1b fix(deps): update dependency vue-i18n to v9.5.0 2023-10-03 12:16:07 +00:00
kolaente 1ad6d5a66b fix(deps): update dependency @intlify/unplugin-vue-i18n to v1 2023-10-03 12:16:07 +00:00
kolaente 4754bb99f0 fix(deps): update dependency marked to v9 2023-10-03 12:16:07 +00:00
kolaente 608e99fffc chore(deps): update pnpm to v8.8.0 2023-10-03 12:16:07 +00:00
kolaente d6741d19e3 fix(deps): update sentry-javascript monorepo to v7.73.0 2023-10-03 12:16:07 +00:00
kolaente ec52be0353 fix(deps): update dependency axios to v1.5.1 2023-10-03 12:16:07 +00:00
renovate 2d5ab4f5f0 chore(deps): update dependency node to v18.18.0 2023-10-03 11:45:51 +00:00
Giacomo Rossetto a71755e408 feat(gantt): implement dynamic sizing on small date ranges (#3750)
Reviewed-on: vikunja/frontend#3750
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Giacomo Rossetto <jackyman_cs4@live.it>
Co-committed-by: Giacomo Rossetto <jackyman_cs4@live.it>
2023-10-03 11:30:24 +00:00
kolaente 66c7a05cdb
fix(project): correctly show project color next to project title in list view
Resolves https://community.vikunja.io/t/color-bubbles-not-showing-after-import/1648
2023-09-29 20:46:02 +02:00
kolaente 287daf9125
fix(auth): silently discard invalid auth tokens and log the user out 2023-09-29 10:38:00 +02:00
kolaente 8507808058
fix(tasks): ignore empty lines when adding multiple tasks at once
Resolves vikunja/frontend#3732 (comment)
2023-09-29 10:06:38 +02:00
kolaente 93c155dd2f
fix(quick actions): always open quick actions with hotkey, even if other inputs are focused
Resolves vikunja/frontend#3743
2023-09-29 09:48:06 +02:00
kolaente b1c4748969
fix(background): unsplash author credit in dark mode 2023-09-29 09:33:30 +02:00
kolaente 0887860b2a
fix(task): priority label spacing 2023-09-29 09:32:00 +02:00
kolaente 0b1c8ed4dd
fix(attachments): layout and coloring in dark mode 2023-09-29 09:28:51 +02:00
kolaente 3988a3f9f8
fix(task): correct spacing to task and project title 2023-09-29 09:19:17 +02:00
Frederick [Bot] 11b65e844c [skip ci] Updated translations via Crowdin 2023-09-28 00:08:46 +00:00
renovate 5c23343172 chore(deps): update dev-dependencies (#3747)
Reviewed-on: vikunja/frontend#3747
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-09-27 07:34:49 +00:00
renovate 01a4335c7c chore(deps): update node.js to v20.7 (#3736)
Reviewed-on: vikunja/frontend#3736
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-09-27 07:34:22 +00:00
Frederick [Bot] 4a2ecf5fe7 [skip ci] Updated translations via Crowdin 2023-09-24 00:29:29 +00:00
renovate 0235b14997 chore(deps): update dev-dependencies (#3746)
Reviewed-on: vikunja/frontend#3746
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-09-15 11:59:29 +00:00
renovate 8eafa23269 chore(deps): update dev-dependencies (#3740)
Reviewed-on: vikunja/frontend#3740
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-09-13 12:35:39 +00:00
kolaente 5c95a721f4
fix(filter): don't show other filters in project selection in saved filter
Resolves https://github.com/go-vikunja/frontend/issues/125
2023-09-13 11:41:47 +02:00
kolaente a6eb804fae
fix(gantt): open task with double click from the gantt chart 2023-09-12 15:12:16 +02:00
Frederick [Bot] 09ffd9414b [skip ci] Updated translations via Crowdin 2023-09-08 00:29:37 +00:00
kolaente b126a7f7ff
fix(navigation): don't hide color bubble in navigation on touch devices
Related discussion: https://community.vikunja.io/t/board-color-sticker/1607
2023-09-07 16:36:11 +02:00
kolaente f256fc3843
fix(list view): align nested subtasks with the parent text 2023-09-07 13:47:52 +02:00
kolaente e41712647d
feat(list view): show subtasks nested
Resolves vikunja/frontend#363
2023-09-07 13:43:15 +02:00
Frederick [Bot] 842e2c2811 [skip ci] Updated translations via Crowdin 2023-09-07 00:28:16 +00:00
kolaente 2d61a349ac
feat(task): immediately set focus on the task search input when opening the related tasks menu 2023-09-06 18:50:38 +02:00
kolaente 54c527c23f
feat(tasks): make the whole task in list view clickable
Resolves vikunja/frontend#3172
Closes vikunja/frontend#3176
Closes vikunja/frontend#3180
2023-09-06 18:31:30 +02:00
kolaente 4d8c6622d2
fix(ci): use correct secret key to push 2023-09-06 18:03:09 +02:00
kolaente 3f3d4b1682
feat(labels): assign random color when creating labels
Resolves F-591
Related discussion: https://community.vikunja.io/t/assign-a-random-color-to-a-new-label/348/7
2023-09-06 17:10:36 +02:00
kolaente 9c46d064ac
feat(quick add magic): allow using the project identifier via quick add magic
Related discussion: https://community.vikunja.io/t/using-shorter-list-names-in-quick-add-magic/895
2023-09-06 16:51:23 +02:00
kolaente 0d3143d465
fix(quick add magic): headline 2023-09-06 16:45:12 +02:00
kolaente 337c3e5e3e
fix: lint 2023-09-06 16:31:07 +02:00
kolaente 3bb5308141
feat(task): group related task action buttons 2023-09-06 16:30:00 +02:00
kolaente 3fec92283b
fix(task): priority label sizing and positioning in different environments 2023-09-06 15:58:52 +02:00
kolaente beb016400e
feat(task): move task priority to the front when showing tasks inline 2023-09-06 15:53:40 +02:00
kolaente 7746d39161
fix(task): remove wrong repeat types
Repeating "monthly" or "yearly" was never what people expected, only 30 or 365 days which is not always correct. This change removes these settings since the repeating modes will be re-done anyway.

Related to vikunja/frontend#3585 (comment)
2023-09-06 15:41:48 +02:00
kolaente b187e8c1b6
fix(ci): pin used node version to 20.5 to avoid build issues
Related https://github.com/vitejs/vite/issues/14299
2023-09-06 15:33:04 +02:00
kolaente 0ecda46af9
chore(deps): update dependencies 2023-09-06 15:30:00 +02:00
kolaente 59dc927b5c
feat(i18n): update translations only once a day 2023-09-06 15:24:44 +02:00
kolaente a13953ee14
fix(i18n): add upload files config 2023-09-06 15:22:51 +02:00
kolaente a4b836d395
feat(i18n): run translation update directly 2023-09-06 15:19:32 +02:00
kolaente 16b46b0f4d
feat(i18n): update crowdin sync to use v2 api 2023-09-06 15:18:27 +02:00
kolaente 184110b986
fix(gantt): update the gantt view when switching between projects
Resolves https://community.vikunja.io/t/listing-subprojects-tasks/1567/5
2023-09-06 13:25:27 +02:00
kolaente 1918947c0b
fix(tasks): reset page number when applying filters
Resolves https://community.vikunja.io/t/when-filter-conditions-change-pages-arent-updated-according-to-new-list-length/1601
2023-09-06 10:50:52 +02:00
kolaente 4e5823183e
fix(tasks): update api route 2023-09-06 10:41:39 +02:00
kolaente b9e17ea870
fix(api tokens): show a token after it was created 2023-09-06 09:59:27 +02:00
Frederick [Bot] a8a6ec5ab0 [skip ci] Updated translations via Crowdin 2023-09-06 00:29:43 +00:00
Frederick [Bot] 3e9b872894 [skip ci] Updated translations via Crowdin 2023-09-05 00:29:24 +00:00
kolaente c4adcf4655
chore: include version json string in release zip 2023-09-04 22:19:37 +02:00
kolaente b1fe3fe29b
fix: don't render route modal when no properties are defined 2023-09-04 21:33:50 +02:00
kolaente 5720a86bc3
fix(task): don't reload the kanban board when opening a task 2023-09-04 21:01:42 +02:00
kolaente 86eff7d49e
fix(task): don't reload the kanban board when opening a task 2023-09-04 20:27:55 +02:00
kolaente 7a9aa7771b
fix(tasks): play pop sound directly and not from store
This solves two problems:

1. Previously, changing anything on a done task would play the pop sound all the time, because the store only knew the new done status was "done" and not if it was done previously already.
2. Safari will prevent playing a sound without user interaction. This means the user has to interact directly with the method playing the sound which was not the case when the sound was played from the store.

Resolves vikunja/frontend#3292
2023-09-04 20:14:43 +02:00
kolaente abbc11528e
feat(tasks): update due date text every minute
Related discussion: https://community.vikunja.io/t/text-describing-time-past-due-date-is-never-refreshed/1376/3
2023-09-04 14:00:22 +02:00
kolaente 725fd1ad46
feat: improve error message for invalid API url
Resolves vikunja/frontend#3680
2023-09-04 13:37:17 +02:00
kolaente 44754fac0f
chore(ci): sign drone config 2023-09-04 13:11:59 +02:00
kolaente 7f2d92138e
fix: lint 2023-09-04 13:11:31 +02:00
kolaente 95be0d1d32
fix(build): don't download Puppeteer when building for prod 2023-09-04 13:07:48 +02:00
kolaente f63c39a578
feat(assignees): improve avatar list consistency
Resolves vikunja/frontend#3354
2023-09-04 13:03:39 +02:00
kolaente 270e32290a
fix(quick add magic): ignore common task indention when adding multiple tasks at once
Resolves vikunja/frontend#3732
2023-09-04 11:24:10 +02:00
kolaente 9cf8696b84
fix(docker): set correct default value for custom logo url 2023-09-04 10:22:44 +02:00
Frederick [Bot] b97e13b6b4 [skip ci] Updated translations via Crowdin 2023-09-04 00:28:15 +00:00
konrad 04ba1011cc feat: add setting for default bucket
Reviewed-on: vikunja/frontend#3735
2023-09-03 15:14:44 +00:00
kolaente 52c0efe0ce
feat(kanban): add icon for bucket collapse 2023-09-03 16:32:29 +02:00
kolaente c803020537
feat(kanban): add setting for default bucket 2023-09-03 16:32:29 +02:00
kolaente 3373b5fc45
feat(kanban): save done bucket with project instead of bucket 2023-09-03 16:32:29 +02:00
kolaente f6d1db3595
fix: tests 2023-09-03 16:30:36 +02:00
Frederick [Bot] ce6f099912 [skip ci] Updated translations via Crowdin 2023-09-03 00:29:23 +00:00
kolaente ed8fb71ff0
feat: add demo mode warning message
Resolves vikunja/frontend#2453
2023-09-01 18:09:19 +02:00
konrad 28f2551d87 feat: api tokens
Reviewed-on: vikunja/frontend#3733
2023-09-01 14:34:56 +00:00
kolaente cec480ad80
fix(api tokens): lint 2023-09-01 15:59:16 +02:00
kolaente 830a3745ba
feat(api tokens): show warning if token has expired 2023-09-01 13:32:00 +02:00
kolaente 49104c65b6
fix(api tokens): expiry of tokens in a number of days 2023-09-01 13:28:32 +02:00
kolaente 984978fe6d
feat(api tokens): format permissions and groups human-readable 2023-09-01 13:25:37 +02:00
kolaente bd7b973559
feat(api tokens): add deleting api tokens 2023-09-01 13:18:00 +02:00
kolaente 0bb85870db
feat(api tokens): allow custom selection of expiry dates 2023-09-01 13:07:20 +02:00
kolaente 021f92303d
feat(api tokens): validate title field when creating a new token 2023-09-01 12:56:23 +02:00
kolaente e47ad021a3
feat(api tokens): add token creation form 2023-09-01 12:47:32 +02:00
kolaente a20eef2453
feat(api tokens): add basic api token overview 2023-09-01 11:15:48 +02:00
Frederick [Bot] 7b57b10804 [skip ci] Updated translations via Crowdin 2023-08-31 00:29:36 +00:00
Frederick [Bot] 83a7032b6f [skip ci] Updated translations via Crowdin 2023-08-30 00:29:17 +00:00
renovate 49261a6fcc chore(deps): update dev-dependencies (#3726)
Reviewed-on: vikunja/frontend#3726
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-08-29 11:59:08 +00:00
kolaente 5630c90dee
fix(task): show related tasks form with shortcut even when there are already other related tasks
Resolves https://github.com/go-vikunja/frontend/issues/122
2023-08-29 13:57:12 +02:00
konrad 47d589002c feat: quick actions improvments
Reviewed-on: vikunja/frontend#3728
2023-08-29 11:24:00 +00:00
kolaente 99e2161c09
fix: lint 2023-08-29 12:46:30 +02:00
kolaente 20f61baf03
fix(quick actions): search for tasks within a project when specifying a project with quick add magic 2023-08-29 12:45:05 +02:00
kolaente 4e6b99544e
fix(quick actions): don't show projects when searching for labels or tasks 2023-08-29 12:38:59 +02:00
kolaente d57e1909c4
feat(quick actions): show labels as labels and tasks with all of their details 2023-08-29 12:33:43 +02:00
kolaente 99d8fbdfa7
feat(quick actions): show tasks for a label when selecting it 2023-08-29 11:11:37 +02:00
kolaente 442d0342a9
fix(quick actions): project search 2023-08-29 10:08:47 +02:00
kolaente a4b369470a
fix(quick actions): invalid class prop 2023-08-29 09:57:13 +02:00
kolaente 0ca73e0851
fix(quick actions): always search for projects 2023-08-29 09:41:53 +02:00
kolaente 9fc829115f
fix(quick actions): project filter 2023-08-29 09:34:08 +02:00
kolaente 1e19548563
chore(quick actions): format 2023-08-29 09:33:56 +02:00
kolaente c327d86a71
feat(quick actions): show task identifier 2023-08-29 09:33:41 +02:00
kolaente 3044560759
feat(quick actions): show done tasks last 2023-08-29 09:21:11 +02:00
kolaente c3f85fcb19
chore: format 2023-08-29 09:19:52 +02:00
renovate 53434952d3 chore(deps): update dev-dependencies (#3721)
Reviewed-on: vikunja/frontend#3721
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-08-27 15:27:00 +00:00
renovate e9b0640660 fix(deps): update dependency @vueuse/core to v10.4.0 (#3723)
Reviewed-on: vikunja/frontend#3723
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-08-27 15:00:38 +00:00
renovate ae57e5d314
chore(deps): update pnpm to v8.7.0 2023-08-27 16:33:22 +02:00
kolaente 6e7928b2e4
fix(i18n): hungarian translation 2023-08-27 16:32:06 +02:00
kolaente 47639b00f8
feat(i18n): add hungarian translation for selection 2023-08-27 10:28:31 +02:00
Frederick [Bot] e63cecceca [skip ci] Updated translations via Crowdin 2023-08-27 00:29:16 +00:00
Frederick [Bot] 55e2e323ed [skip ci] Updated translations via Crowdin 2023-08-26 00:29:30 +00:00
kolaente f7e22c8c56
fix(auth): correctly redirect the user to the last visited page after login
Resolves vikunja/frontend#3682
2023-08-24 12:15:45 +02:00
kolaente a9fb306e46
fix(i18n): fall back to browser language if the configured user language is invalid 2023-08-24 11:37:23 +02:00
kolaente 58a1f46668
fix(projects): don't suggest to create a new task in an empty filter 2023-08-24 11:32:28 +02:00
kolaente 6cbbe17bd8
fix(filters): don't allow marking a filter as favorite 2023-08-24 11:30:57 +02:00
kolaente c01957aae2
fix: lint 2023-08-24 11:27:31 +02:00
kolaente 1ad03877fb
fix(menu): separate favorite and saved filter projects from other projects
Resolves vikunja/frontend#3710
Resolves https://github.com/go-vikunja/frontend/issues/119
2023-08-24 11:27:20 +02:00
kolaente fc72a82a2a
fix(task): duplicate attribute 2023-08-24 11:18:17 +02:00
kolaente 63ef09b020
fix(filters): incorrect translation string 2023-08-24 11:18:03 +02:00
DIMITRIOS CHRYSOCHERIS 311b1d7594
chore: improve checking for API url '/' suffix (#121) 2023-08-23 21:56:08 +02:00
davidangel cade3df3e9 feat: allow custom logo via environment variable (#3685)
Related discussion: https://community.vikunja.io/t/change-vikunja-logo-and-color-scheme/621

Reviewed-on: vikunja/frontend#3685
Reviewed-by: konrad <k@knt.li>
Co-authored-by: davidangel <david@davidangel.net>
Co-committed-by: davidangel <david@davidangel.net>
2023-08-23 16:13:29 +00:00
kolaente 37975c1931 chore(deps): update lockfile 2023-08-23 06:37:45 +00:00
renovate 0d500182e7 chore(deps): update dev-dependencies 2023-08-23 06:37:45 +00:00
Frederick [Bot] f647d6e9b4 [skip ci] Updated translations via Crowdin 2023-08-23 00:28:13 +00:00
renovate dbed4caca7 fix(deps): update dependency ufo to v1.2.0 2023-08-21 11:06:04 +00:00
renovate 6d79c9b2ed chore(deps): update pnpm to v8.6.12 2023-08-21 11:03:59 +00:00
renovate 24f0822a12 fix(deps): update dependency @vueuse/core to v10.3.0 2023-08-21 11:03:34 +00:00
renovate f3ba778fd3 fix(deps): update dependency pinia to v2.1.6 2023-08-21 11:03:08 +00:00
renovate 55a7255728 fix(deps): update font awesome to v6.4.2 2023-08-21 10:58:33 +00:00
renovate 2b47e5faec chore(deps): update node.js to v18.17.1 2023-08-21 10:58:19 +00:00
kolaente 9f82ec4162
chore: update lockfile 2023-08-21 12:22:58 +02:00
kolaente 64c90c7fe8
chore: update flake 2023-08-21 12:22:30 +02:00
renovate 9fe3d2b2bc chore(deps): update dev-dependencies 2023-08-21 09:06:05 +00:00
Frederick [Bot] 0b1ec9f287 [skip ci] Updated translations via Crowdin 2023-08-16 00:09:52 +00:00
Frederick [Bot] baff1c6fc9 [skip ci] Updated translations via Crowdin 2023-08-15 00:08:33 +00:00
Frederick [Bot] ac3f0cc266 [skip ci] Updated translations via Crowdin 2023-08-07 00:08:30 +00:00
renovate 7e1cfebf6a chore(deps): update dependency @types/node to v18.17.0 2023-07-23 01:29:55 +00:00
Frederick [Bot] 88203e8b7d [skip ci] Updated translations via Crowdin 2023-07-23 00:09:21 +00:00
renovate d466d50712 chore(deps): update dev-dependencies 2023-07-22 01:04:58 +00:00
renovate cf945f2841 fix(deps): update sentry-javascript monorepo to v7.60.0 2023-07-21 14:06:11 +00:00
renovate 74df69fc94 chore(deps): update dev-dependencies 2023-07-21 12:04:48 +00:00
Frederick [Bot] 563ee8f5bc [skip ci] Updated translations via Crowdin 2023-07-21 00:09:38 +00:00
renovate 026db7acad chore(deps): update node.js to v18.17.0 2023-07-20 07:24:20 +00:00
renovate 61e97bfe1c fix(deps): update sentry-javascript monorepo to v7.59.3 2023-07-19 17:04:49 +00:00
renovate 6530d26b82 chore(deps): update pnpm to v8.6.9 2023-07-18 11:04:21 +00:00
renovate 1e24fe8bab fix(deps): update sentry-javascript monorepo to v7.59.2 2023-07-18 09:04:49 +00:00
renovate f786c2b8a2 chore(deps): update dev-dependencies 2023-07-18 08:50:37 +00:00
renovate e596e2c3bc fix(deps): update sentry-javascript monorepo to v7.59.1 2023-07-18 08:04:48 +00:00
Frederick [Bot] a5e49d9417 [skip ci] Updated translations via Crowdin 2023-07-18 00:08:41 +00:00
renovate 668b910190 chore(deps): update pnpm to v8.6.8 2023-07-17 10:04:41 +00:00
renovate 2bdc532f89 fix(deps): update dependency codemirror to v5.65.14 2023-07-17 08:04:36 +00:00
renovate 933c7d8acc chore(deps): update dev-dependencies 2023-07-16 04:28:21 +00:00
Frederick [Bot] 253e716390 [skip ci] Updated translations via Crowdin 2023-07-16 00:09:30 +00:00
renovate d19a5d9714 chore(deps): update dev-dependencies 2023-07-15 00:04:50 +00:00
renovate 90cad1c8dd chore(deps): update dev-dependencies 2023-07-14 01:09:00 +00:00
Frederick [Bot] 057017c8eb [skip ci] Updated translations via Crowdin 2023-07-14 00:08:42 +00:00
kolaente d7ce8dd320
fix(quick add magic): repeating intervals in words
Resolves vikunja/frontend#3676
2023-07-13 18:20:30 +02:00
kolaente 25b110ce48
fix(quick add magic): annually and variants spelling
Related to vikunja/frontend#3676
2023-07-13 18:05:19 +02:00
renovate 33fe5e4f20 fix(deps): update sentry-javascript monorepo to v7.58.1 2023-07-13 12:04:38 +00:00
renovate 129ef769a3 fix(deps): update sentry-javascript monorepo to v7.58.0 2023-07-13 06:47:22 +00:00
renovate 9030a9f7c1 chore(deps): update dev-dependencies 2023-07-13 02:05:04 +00:00
Frederick [Bot] 3748a496d5 [skip ci] Updated translations via Crowdin 2023-07-13 00:08:55 +00:00
renovate 890e7e1f52 chore(deps): update dependency vite to v4.4.3 2023-07-12 00:05:46 +00:00
renovate 9e0f2b0249 chore(deps): update dev-dependencies 2023-07-11 11:04:54 +00:00
renovate 9a34c522b2 fix(deps): update dependency dompurify to v3.0.5 2023-07-11 10:24:42 +00:00
renovate 60dd698fad chore(deps): update dev-dependencies to v6 2023-07-11 10:05:43 +00:00
kolaente 15ecafdf04
fix: don't try to load buckets for project id 0 2023-07-11 10:42:20 +02:00
162 changed files with 14149 additions and 5105 deletions

View File

@ -42,7 +42,7 @@ steps:
# - .cache
- name: dependencies
image: node:20-alpine
image: node:20.9-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -55,7 +55,7 @@ steps:
# - restore-cache
- name: lint
image: node:20-alpine
image: node:20.9-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -66,7 +66,7 @@ steps:
- dependencies
- name: build-prod
image: node:20-alpine
image: node:20.9-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -77,7 +77,7 @@ steps:
- dependencies
- name: test-unit
image: node:20-alpine
image: node:20.9-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-alpine
image: node:20.9-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -202,7 +202,7 @@ steps:
# - .cache
- name: build
image: node:20-alpine
image: node:20.9-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -210,6 +210,7 @@ steps:
from_secret: sentry_auth_token
SENTRY_ORG: vikunja
SENTRY_PROJECT: frontend-oss
PUPPETEER_SKIP_DOWNLOAD: true
commands:
- apk add git
- corepack enable && pnpm config set store-dir .cache/pnpm
@ -225,6 +226,7 @@ steps:
image: kolaente/zip
pull: always
commands:
- cp src/version.json dist
- cd dist
- zip -r ../vikunja-frontend-unstable.zip *
- cd ..
@ -283,7 +285,7 @@ steps:
# - .cache
- name: build
image: node:20-alpine
image: node:20.9-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -306,6 +308,7 @@ steps:
image: kolaente/zip
pull: always
commands:
- cp src/version.json dist
- cd dist
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
- cd ..
@ -472,24 +475,25 @@ name: update-translations
trigger:
branch:
- main
include:
- main
event:
- cron
include:
- cron
cron:
- update_translations
steps:
- name: download
pull: always
image: jonasfranz/crowdin
image: git.lcomrade.su/root/drone-crowdin-v2
settings:
download: true
export_dir: src/i18n/lang/
ignore_branch: true
project_identifier: vikunja
environment:
CROWDIN_KEY:
crowdin_key:
from_secret: crowdin_key
project_id: 462614
target: download
download_to: src/i18n/lang/
download_export_approved_only: true
- name: move-files
pull: always
@ -512,23 +516,22 @@ steps:
commit_message: "[skip ci] Updated translations via Crowdin"
remote: "ssh://git@kolaente.dev:9022/vikunja/frontend.git"
ssh_key:
from_secret: translation_git_push_ssh_key
from_secret: git_push_ssh_key
- name: upload
pull: always
image: jonasfranz/crowdin
image: git.lcomrade.su/root/drone-crowdin-v2
depends_on:
- clone
settings:
files:
en.json: src/i18n/lang/en.json
ignore_branch: true
project_identifier: vikunja
environment:
CROWDIN_KEY:
from_secret: crowdin_key
crowdin_key:
from_secret: crowdin_key
project_id: 462614
target: upload
upload_files:
src/i18n/lang/en.json: en.json
---
kind: signature
hmac: 6a566550cac03e9f3f9bbccab95fda4b342233bd63a1409cb5f634b1c744c326
hmac: dab902060979f246df77641c995c843ea39f86dba2de9003da7e593ce6f6f08a
...

23
.github/workflows/lockdown.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: 'Repo Lockdown'
on:
pull_request_target:
types: opened
permissions:
issues: write
pull-requests: write
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/repo-lockdown@v3
with:
pr-comment: 'Hi! Thank you for your contribution.
This repo is only a mirror and unfortunately we can''t accept PRs made here. Please re-submit your changes to [our Gitea instance](https://kolaente.dev/vikunja/frontend/pulls).
Also check out the [contribution guidelines](https://vikunja.io/docs/development/#pull-requests).
Thank you for your understanding.'

2
.nvmrc
View File

@ -1 +1 @@
18.16.1
20.9.0

View File

@ -3,13 +3,14 @@
# │─││ │││ │ │
# ┘─┘┘─┘┘┘─┘┘─┘
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
FROM --platform=$BUILDPLATFORM node:20.9-alpine AS builder
WORKDIR /build
ARG USE_RELEASE=false
ARG RELEASE_VERSION=unstable
ENV PNPM_CACHE_FOLDER .cache/pnpm/
ENV PUPPETEER_SKIP_DOWNLOAD true
COPY package.json ./
COPY pnpm-lock.yaml ./
@ -57,6 +58,7 @@ ENV VIKUNJA_SENTRY_ENABLED false
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
ENV VIKUNJA_ALLOW_ICON_CHANGES true
ENV VIKUNJA_CUSTOM_LOGO_URL "''"
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh

View File

View File

@ -1,40 +0,0 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
import {ProjectFactory} from '../../factories/project'
import {UserProjectFactory} from '../../factories/users_project'
import {BucketFactory} from '../../factories/bucket'
describe('Editor', () => {
createFakeUserAndLogin()
beforeEach(() => {
ProjectFactory.create(1)
BucketFactory.create(1)
TaskFactory.truncate()
UserProjectFactory.truncate()
})
it('Has a preview with checkable checkboxes', () => {
const tasks = TaskFactory.create(1, {
description: `# Test Heading
* Bullet 1
* Bullet 2
* [ ] Checklist
* [x] Checklist checked
`,
bucket_id: 1,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('input[type=checkbox][data-checkbox-num=0]')
.click()
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
.contains('Saved!')
.should('exist')
cy.get('.preview.content')
.should('contain', 'Test Heading')
})
})

View File

@ -24,7 +24,7 @@ function addLabelToTaskAndVerify(labelTitle: string) {
.first()
.click()
cy.get('.global-notification', { timeout: 4000 })
cy.get('.global-notification', {timeout: 4000})
.should('contain', 'Success')
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag')
.should('exist')
@ -122,7 +122,7 @@ describe('Task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
index: 1,
description: 'Lorem ipsum dolor sit amet.'
description: 'Lorem ipsum dolor sit amet.',
})
cy.visit(`/tasks/${tasks[0].id}`)
@ -143,7 +143,7 @@ describe('Task', () => {
id: 1,
index: 1,
done: true,
done_at: new Date().toISOString()
done_at: new Date().toISOString(),
})
cy.visit(`/tasks/${tasks[0].id}`)
@ -196,13 +196,13 @@ describe('Task', () => {
it('Can edit the description', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: 'Lorem ipsum dolor sit amet.'
description: 'Lorem ipsum dolor sit amet.',
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.content.description .editor button')
cy.get('.task-view .details.content.description .tiptap button.done-edit')
.click()
cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
cy.get('.task-view .details.content.description .tiptap__editor .tiptap.ProseMirror')
.type('{selectall}New Description')
cy.get('[data-cy="saveEditor"]')
.contains('Save')
@ -219,7 +219,7 @@ describe('Task', () => {
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .comments .media.comment .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
cy.get('.task-view .comments .media.comment .tiptap__editor .tiptap.ProseMirror')
.should('be.visible')
.type('{selectall}New Comment')
cy.get('.task-view .comments .media.comment .button:not([disabled])')
@ -227,7 +227,7 @@ describe('Task', () => {
.should('be.visible')
.click()
cy.get('.task-view .comments .media.comment .editor')
cy.get('.task-view .comments .media.comment .tiptap__editor')
.should('contain', 'New Comment')
cy.get('.global-notification')
.should('contain', 'Success')
@ -236,7 +236,7 @@ describe('Task', () => {
it('Can move a task to another project', () => {
const projects = ProjectFactory.create(2)
BucketFactory.create(2, {
project_id: '{increment}'
project_id: '{increment}',
})
const tasks = TaskFactory.create(1, {
id: 1,
@ -380,7 +380,7 @@ describe('Task', () => {
addLabelToTaskAndVerify(labels[0].title)
})
it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
const tasks = TaskFactory.create(1, {
id: 1,
@ -389,18 +389,18 @@ describe('Task', () => {
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
cy.visit(`/projects/${projects[0].id}/kanban`)
cy.get('.bucket .task')
.contains(tasks[0].title)
.click()
addLabelToTaskAndVerify(labels[0].title)
cy.get('.modal-content .close')
.click()
cy.get('.bucket .task')
.should('contain.text', labels[0].title)
})
@ -461,7 +461,7 @@ describe('Task', () => {
cy.get('.global-notification')
.should('contain', 'Success')
})
it('Can set a reminder', () => {
TaskReminderFactory.truncate()
const tasks = TaskFactory.create(1, {
@ -543,7 +543,7 @@ describe('Task', () => {
cy.get('.global-notification')
.should('contain', 'Success')
})
it('Allows to set a custom relative reminder when the task already has a due date', () => {
TaskReminderFactory.truncate()
const tasks = TaskFactory.create(1, {
@ -579,7 +579,7 @@ describe('Task', () => {
cy.get('.global-notification')
.should('contain', 'Success')
})
it('Allows to set a fixed reminder when the task already has a due date', () => {
TaskReminderFactory.truncate()
const tasks = TaskFactory.create(1, {
@ -609,7 +609,7 @@ describe('Task', () => {
cy.get('.global-notification')
.should('contain', 'Success')
})
it('Can set a priority for a task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
@ -647,7 +647,7 @@ describe('Task', () => {
.select('50%')
cy.get('.global-notification')
.should('contain', 'Success')
cy.wait(200)
cy.get('.task-view .columns.details .column')
@ -656,7 +656,7 @@ describe('Task', () => {
.should('be.visible')
.should('have.value', '0.5')
})
it('Can add an attachment to a task', () => {
TaskAttachmentFactory.truncate()
const tasks = TaskFactory.create(1, {
@ -691,35 +691,119 @@ describe('Task', () => {
cy.get('.bucket .task .footer .icon svg.fa-paperclip')
.should('exist')
})
it('Can check items off a checklist', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: `
This is a checklist:
* [ ] one item
* [ ] another item
* [ ] third item
* [ ] fourth item
* [x] and this one is already done
`,
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>First Item</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Second Item</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Third Item</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Fourth Item</p></div>
</li>
<li data-checked="true" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Fifth Item</p></div>
</li>
</ul>`,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .checklist-summary')
.should('contain.text', '1 of 5 tasks')
cy.get('.editor .content ul > li input[type=checkbox]')
cy.get('.tiptap__editor ul > li input[type=checkbox]')
.eq(2)
.click()
cy.get('.editor .content ul > li input[type=checkbox]')
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
.contains('Saved!')
.should('exist')
cy.get('.tiptap__editor ul > li input[type=checkbox]')
.eq(2)
.should('be.checked')
cy.get('.editor .content input[type=checkbox]')
cy.get('.tiptap__editor input[type=checkbox]')
.should('have.length', 5)
cy.get('.task-view .checklist-summary')
.should('contain.text', '2 of 5 tasks')
})
it('Should use the editor to render description', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: `
<h1>Lorem Ipsum</h1>
<p>Dolor sit amet</p>
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>First Item</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Second Item</p></div>
</li>
</ul>`,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.tiptap__editor ul > li input[type=checkbox]')
.should('exist')
cy.get('.tiptap__editor h1')
.contains('Lorem Ipsum')
.should('exist')
cy.get('.tiptap__editor p')
.contains('Dolor sit amet')
.should('exist')
})
it.only('Should render an image from attachment', async () => {
TaskAttachmentFactory.truncate()
const tasks = TaskFactory.create(1, {
id: 1,
description: '',
})
cy.readFile('cypress/fixtures/image.jpg', null).then(file => {
const formData = new FormData()
formData.append('files', new Blob([file]), 'image.jpg')
cy.request({
method: 'PUT',
url: `${Cypress.env('API_URL')}/tasks/${tasks[0].id}/attachments`,
headers: {
'Authorization': `Bearer ${window.localStorage.getItem('token')}`,
'Content-Type': 'multipart/form-data',
},
body: formData,
})
.then(({body}) => {
const dec = new TextDecoder('utf-8')
const {success} = JSON.parse(dec.decode(body))
TaskFactory.create(1, {
id: 1,
description: `<img src="${Cypress.env('API_URL')}/tasks/${tasks[0].id}/attachments/${success[0].id}" alt="test image">`,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.tiptap__editor img')
.should('be.visible')
.and(($img) => {
// "naturalWidth" and "naturalHeight" are set when the image loads
expect($img[0].naturalWidth).to.be.greaterThan(0)
})
})
})
})
})
})

View File

@ -1,4 +1,5 @@
import {UserFactory} from '../../factories/user'
import {ProjectFactory} from '../../factories/project'
const testAndAssertFailed = fixture => {
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
@ -13,26 +14,28 @@ const testAndAssertFailed = fixture => {
cy.get('div.message.danger').contains('Wrong username or password.')
}
const username = 'test'
const credentials = {
username: 'test',
password: '1234',
}
function login() {
cy.get('input[id=username]').type(credentials.username)
cy.get('input[id=password]').type(credentials.password)
cy.get('.button').contains('Login').click()
cy.url().should('include', '/')
}
context('Login', () => {
beforeEach(() => {
UserFactory.create(1, {username})
UserFactory.create(1, {username: credentials.username})
})
it('Should log in with the right credentials', () => {
const fixture = {
username: 'test',
password: '1234',
}
cy.visit('/login')
cy.get('input[id=username]').type(fixture.username)
cy.get('input[id=password]').type(fixture.password)
cy.get('.button').contains('Login').click()
cy.url().should('include', '/')
login()
cy.clock(1625656161057) // 13:00
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
cy.get('h2').should('contain', `Hi ${credentials.username}!`)
})
it('Should fail with a bad password', () => {
@ -57,4 +60,15 @@ context('Login', () => {
cy.visit('/')
cy.url().should('include', '/login')
})
it('Should redirect to the previous route after logging in', () => {
const projects = ProjectFactory.create(1)
cy.visit(`/projects/${projects[0].id}/list`)
cy.url().should('include', '/login')
login()
cy.url().should('include', `/projects/${projects[0].id}/list`)
})
})

View File

@ -17,7 +17,7 @@ context('Registration', () => {
it('Should work without issues', () => {
const fixture = {
username: 'testuser',
password: '123456',
password: '12345678',
email: 'testuser@example.com',
}
@ -31,10 +31,10 @@ context('Registration', () => {
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
})
it.only('Should fail', () => {
it('Should fail', () => {
const fixture = {
username: 'test',
password: '123456',
password: '12345678',
email: 'testuser@example.com',
}

View File

@ -13,5 +13,6 @@ sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g"
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.PROJECT_INFINITE_NESTING_ENABLED\s*=)\s*.+:\1 '${VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED}':g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.ALLOW_ICON_CHANGES\s*=)\s*.+:\1 ${VIKUNJA_ALLOW_ICON_CHANGES}:g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.CUSTOM_LOGO_URL\s*=)\s*.+:\1 ${VIKUNJA_CUSTOM_LOGO_URL}:g" /usr/share/nginx/html/index.html
date -uIseconds | xargs echo 'info: started at'

View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1685498995,
"narHash": "sha256-rdyjnkq87tJp+T2Bm1OD/9NXKSsh/vLlPeqCc/mm7qs=",
"lastModified": 1697730408,
"narHash": "sha256-Ww//zzukdTrwTrCUkaJA/NsaLEfUfQpWZXBdXBYfhak=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9cfaa8a1a00830d17487cb60a19bb86f96f09b27",
"rev": "ff0a5a776b56e0ca32d47a4a47695452ec7f7d80",
"type": "github"
},
"original": {

View File

@ -32,6 +32,8 @@
window.PROJECT_INFINITE_NESTING_ENABLED = false
// Allow changing the logo and other icons based on various occasions throughout the year.
window.ALLOW_ICON_CHANGES = true
// Allow using a custom logo via external URL.
window.CUSTOM_LOGO_URL = ''
</script>
</body>
</html>

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.6.7",
"packageManager": "pnpm@8.10.5",
"keywords": [
"todo",
"productivity",
@ -45,102 +45,137 @@
"story:preview": "histoire preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/vue-fontawesome": "3.0.3",
"@github/hotkey": "2.0.1",
"@infectoone/vue-ganttastic": "2.1.4",
"@intlify/unplugin-vue-i18n": "0.12.2",
"@kyvg/vue3-notification": "2.9.1",
"@sentry/tracing": "7.57.0",
"@sentry/vue": "7.57.0",
"@vueuse/core": "10.2.1",
"axios": "1.4.0",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/vue-fontawesome": "3.0.5",
"@github/hotkey": "2.3.0",
"@infectoone/vue-ganttastic": "2.2.0",
"@intlify/unplugin-vue-i18n": "1.5.0",
"@kyvg/vue3-notification": "3.0.2",
"@sentry/tracing": "7.80.1",
"@sentry/vue": "7.80.1",
"@tiptap/core": "2.1.12",
"@tiptap/extension-blockquote": "2.1.12",
"@tiptap/extension-bold": "2.1.12",
"@tiptap/extension-bullet-list": "2.1.12",
"@tiptap/extension-code": "2.1.12",
"@tiptap/extension-code-block-lowlight": "2.1.12",
"@tiptap/extension-document": "2.1.12",
"@tiptap/extension-dropcursor": "2.1.12",
"@tiptap/extension-gapcursor": "2.1.12",
"@tiptap/extension-hard-break": "2.1.12",
"@tiptap/extension-heading": "2.1.12",
"@tiptap/extension-history": "2.1.12",
"@tiptap/extension-horizontal-rule": "2.1.12",
"@tiptap/extension-image": "2.1.12",
"@tiptap/extension-italic": "2.1.12",
"@tiptap/extension-link": "2.1.12",
"@tiptap/extension-list-item": "2.1.12",
"@tiptap/extension-ordered-list": "2.1.12",
"@tiptap/extension-paragraph": "2.1.12",
"@tiptap/extension-placeholder": "2.1.12",
"@tiptap/extension-strike": "2.1.12",
"@tiptap/extension-table": "2.1.12",
"@tiptap/extension-table-cell": "2.1.12",
"@tiptap/extension-table-header": "2.1.12",
"@tiptap/extension-table-row": "2.1.12",
"@tiptap/extension-task-item": "2.1.12",
"@tiptap/extension-task-list": "2.1.12",
"@tiptap/extension-text": "2.1.12",
"@tiptap/extension-typography": "2.1.12",
"@tiptap/extension-underline": "2.1.12",
"@tiptap/pm": "2.1.12",
"@tiptap/suggestion": "2.1.12",
"@tiptap/vue-3": "2.1.12",
"@types/is-touch-device": "1.0.2",
"@types/lodash.clonedeep": "4.5.9",
"@types/sortablejs": "1.15.5",
"@vueuse/core": "10.5.0",
"@vueuse/router": "10.5.0",
"axios": "1.6.0",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.13",
"date-fns": "2.30.0",
"dayjs": "1.11.9",
"dompurify": "3.0.4",
"easymde": "2.18.0",
"dayjs": "1.11.10",
"dompurify": "3.0.6",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
"floating-vue": "2.0.0-beta.24",
"highlight.js": "11.8.0",
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
"marked": "5.1.1",
"pinia": "2.1.4",
"lowlight": "2.9.0",
"pinia": "2.1.7",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.0",
"ufo": "1.1.2",
"vue": "3.3.4",
"tippy.js": "6.3.7",
"ufo": "1.3.2",
"vue": "3.3.8",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.3",
"vue-i18n": "9.2.2",
"vue-router": "4.2.4",
"vue-i18n": "9.7.0",
"vue-router": "4.2.5",
"workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.4",
"@cypress/vite-dev-server": "5.0.5",
"@cypress/vue": "5.0.5",
"@faker-js/faker": "8.0.2",
"@histoire/plugin-screenshot": "0.16.1",
"@histoire/plugin-vue": "0.16.1",
"@rushstack/eslint-patch": "1.3.2",
"@tsconfig/node18": "18.2.0",
"@types/codemirror": "5.60.8",
"@types/dompurify": "3.0.2",
"@types/flexsearch": "0.7.3",
"@types/is-touch-device": "1.0.0",
"@types/lodash.debounce": "4.0.7",
"@types/marked": "5.0.0",
"@types/node": "18.16.19",
"@4tw/cypress-drag-drop": "2.2.5",
"@cypress/vite-dev-server": "5.0.6",
"@cypress/vue": "6.0.0",
"@faker-js/faker": "8.3.1",
"@histoire/plugin-screenshot": "0.17.0",
"@histoire/plugin-vue": "0.17.5",
"@rushstack/eslint-patch": "1.5.1",
"@tsconfig/node18": "18.2.2",
"@types/codemirror": "5.60.13",
"@types/dompurify": "3.0.5",
"@types/flexsearch": "0.7.6",
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.9.1",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.1",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"@vitejs/plugin-legacy": "4.1.0",
"@vitejs/plugin-vue": "4.2.3",
"@vue/eslint-config-typescript": "11.0.3",
"@vue/test-utils": "2.4.0",
"@types/sortablejs": "1.15.5",
"@typescript-eslint/eslint-plugin": "6.11.0",
"@typescript-eslint/parser": "6.11.0",
"@vitejs/plugin-legacy": "4.1.1",
"@vitejs/plugin-vue": "4.5.0",
"@vue/eslint-config-typescript": "12.0.0",
"@vue/test-utils": "2.4.2",
"@vue/tsconfig": "0.4.0",
"autoprefixer": "10.4.14",
"browserslist": "4.21.9",
"caniuse-lite": "1.0.30001514",
"autoprefixer": "10.4.16",
"browserslist": "4.22.1",
"caniuse-lite": "1.0.30001563",
"css-has-pseudo": "6.0.0",
"csstype": "3.1.2",
"cypress": "12.17.0",
"esbuild": "0.18.11",
"eslint": "8.44.0",
"eslint-plugin-vue": "9.15.1",
"happy-dom": "10.0.3",
"histoire": "0.16.2",
"postcss": "8.4.25",
"cypress": "13.5.1",
"esbuild": "0.19.5",
"eslint": "8.53.0",
"eslint-plugin-vue": "9.18.1",
"happy-dom": "12.10.3",
"histoire": "0.17.5",
"postcss": "8.4.31",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.0",
"postcss-preset-env": "9.0.0",
"rollup": "3.26.2",
"postcss-preset-env": "9.3.0",
"rollup": "4.4.1",
"rollup-plugin-visualizer": "5.9.2",
"sass": "1.63.6",
"start-server-and-test": "2.0.0",
"typescript": "5.1.6",
"vite": "4.4.2",
"vite-plugin-inject-preload": "1.3.1",
"vite-plugin-pwa": "0.16.4",
"sass": "1.69.5",
"start-server-and-test": "2.0.3",
"typescript": "5.2.2",
"vite": "4.5.0",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.16.7",
"vite-plugin-sentry": "1.3.0",
"vite-svg-loader": "4.0.0",
"vitest": "0.33.0",
"vue-tsc": "1.8.4",
"wait-on": "7.0.1",
"vitest": "0.34.6",
"vue-tsc": "1.8.22",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
},
"pnpm": {

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,13 @@
"histoire"
]
},
{
"groupName": "tiptap",
"matchPackagePrefixes": [
"@tiptap/",
"tiptap"
]
},
{
"matchDepTypes": ["devDependencies"],
"groupName": "dev-dependencies",

View File

@ -15,6 +15,7 @@
<AddToHomeScreen/>
<UpdateNotification/>
<Notification/>
<DemoMode/>
</Teleport>
</ready>
</template>
@ -45,6 +46,7 @@ import {useBaseStore} from '@/stores/base'
import {useColorScheme} from '@/composables/useColorScheme'
import {useBodyClass} from '@/composables/useBodyClass'
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
import DemoMode from '@/components/home/DemoMode.vue'
const baseStore = useBaseStore()
const authStore = useAuthStore()

View File

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

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import {computed, ref} from 'vue'
import {useConfigStore} from '@/stores/config'
import BaseButton from '@/components/base/BaseButton.vue'
const configStore = useConfigStore()
const hide = ref(false)
const enabled = computed(() => configStore.demoModeEnabled && !hide.value)
</script>
<template>
<div
v-if="enabled"
class="demo-mode-banner"
>
<p>
{{ $t('demo.title') }}
<strong class="is-uppercase">{{ $t('demo.everythingWillBeDeleted') }}</strong>
</p>
<BaseButton @click="() => hide = true" class="hide-button">
<icon icon="times"/>
</BaseButton>
</div>
</template>
<style scoped lang="scss">
.demo-mode-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--danger);
z-index: 100;
padding: .5rem;
text-align: center;
&, strong {
color: hsl(220, 13%, 91%) !important; // --grey-200 in light mode, hardcoded because the color should not change
}
}
.hide-button {
padding: .25rem .5rem;
cursor: pointer;
position: absolute;
right: .5rem;
top: .25rem;
}
</style>

View File

@ -9,15 +9,21 @@ import {MILLISECONDS_A_HOUR} from '@/constants/date'
const now = useNow({
interval: MILLISECONDS_A_HOUR,
})
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 6 ? LogoFullPride : LogoFull)
const CustomLogo = computed(() => window.CUSTOM_LOGO_URL)
</script>
<template>
<Logo alt="Vikunja" class="logo" />
<div>
<Logo v-if="!CustomLogo" alt="Vikunja" class="logo" />
<img v-show="CustomLogo" :src="CustomLogo" alt="Vikunja" class="logo" />
</div>
</template>
<style lang="scss" scoped>
.logo {
color: var(--logo-text-color);
max-width: 168px;
max-height: 48px;
}
</style>

View File

@ -3,7 +3,7 @@
class="menu-show-button"
@click="baseStore.toggleMenu()"
@shortkey="() => baseStore.toggleMenu()"
v-shortcut="'Control+e'"
v-shortcut="'Mod+e'"
:title="$t('keyboardShortcuts.toggleMenu')"
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
/>

View File

@ -147,6 +147,10 @@ const canNestDeeper = computed(() => canNestProjectDeeper(level))
}
}
.is-touch .color-bubble {
opacity: 1 !important;
}
.color-bubble-handle-wrapper {
position: relative;
width: 1rem;
@ -175,4 +179,8 @@ const canNestDeeper = computed(() => canNestProjectDeeper(level))
color: var(--grey-300) !important;
font-size: .75rem;
}
.is-touch .handle.has-color-bubble {
display: none !important;
}
</style>

View File

@ -27,10 +27,7 @@
</div>
<div class="navbar-end">
<BaseButton @click="openQuickActions" class="trigger-button" v-shortcut="'Control+k'"
:title="$t('keyboardShortcuts.quickSearch')">
<icon icon="search" />
</BaseButton>
<OpenQuickActions/>
<Notifications />
<dropdown>
<template #trigger="{ toggleOpen, open }">
@ -80,6 +77,7 @@ import Notifications from '@/components/notifications/notifications.vue'
import Logo from '@/components/home/Logo.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import MenuButton from '@/components/home/MenuButton.vue'
import OpenQuickActions from '@/components/misc/OpenQuickActions.vue'
import { getProjectTitle } from '@/helpers/getProjectTitle'
@ -98,10 +96,6 @@ const authStore = useAuthStore()
const configStore = useConfigStore()
const imprintUrl = computed(() => configStore.legal.imprintUrl)
const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl)
function openQuickActions() {
baseStore.setQuickActionsActive(true)
}
</script>
<style lang="scss" scoped>

View File

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

View File

@ -60,6 +60,14 @@
:can-collapse="false"
/>
</nav>
<nav class="menu" v-if="savedFilterProjects">
<ProjectsNavigation
:model-value="savedFilterProjects"
:can-edit-order="false"
:can-collapse="false"
/>
</nav>
<nav class="menu">
<ProjectsNavigation
@ -91,6 +99,7 @@ const projectStore = useProjectStore()
const projects = computed(() => projectStore.notArchivedRootProjects)
const favoriteProjects = computed(() => projectStore.favoriteProjects)
const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
</script>
<style lang="scss" scoped>

View File

@ -1,3 +1,5 @@
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
export default createAsyncComponent(() => import('@/components/input/editor.vue'))
const TipTap = createAsyncComponent(() => import('@/components/input/editor/TipTap.vue'))
export default TipTap

View File

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

View File

@ -1,444 +0,0 @@
<template>
<div class="editor">
<div class="clear"></div>
<vue-easymde
:configs="config"
@change="() => bubbleNow()"
@update:modelValue="handleInput"
class="content"
v-if="isEditActive"
v-model="text"/>
<div class="preview content" v-html="preview" v-if="isPreviewActive && text !== ''">
</div>
<p class="has-text-centered has-text-grey is-italic my-5" v-if="showPreviewText">
{{ emptyText }}
<template v-if="isEditEnabled">
<ButtonLink
@click="toggleEdit"
v-shortcut="editShortcut"
class="d-print-none">
{{ $t('input.editor.edit') }}
</ButtonLink>.
</template>
</p>
<ul class="actions d-print-none" v-if="bottomActions.length > 0">
<li v-if="isEditEnabled && !showPreviewText && showSave">
<BaseButton
v-if="showEditButton"
@click="toggleEdit"
v-shortcut="editShortcut">
{{ $t('input.editor.edit') }}
</BaseButton>
<BaseButton
v-else-if="isEditActive"
@click="bubbleSaveClick"
class="done-edit">
{{ $t('misc.save') }}
</BaseButton>
</li>
<li v-for="(action, k) in bottomActions" :key="k">
<BaseButton @click="action.action">{{ action.title }}</BaseButton>
</li>
</ul>
<template v-else-if="isEditEnabled && showSave">
<ul v-if="showEditButton" class="actions d-print-none">
<li>
<BaseButton
@click="toggleEdit"
v-shortcut="editShortcut">
{{ $t('input.editor.edit') }}
</BaseButton>
</li>
</ul>
<x-button
v-else-if="isEditActive"
@click="bubbleSaveClick"
variant="secondary"
:shadow="false"
v-cy="'saveEditor'">
{{ $t('misc.save') }}
</x-button>
</template>
</div>
</template>
<script setup lang="ts">
import {computed, nextTick, onMounted, ref, toRefs, watch} from 'vue'
import VueEasymde from './vue-easymde.vue'
import {marked} from 'marked'
import DOMPurify from 'dompurify'
import {createEasyMDEConfig} from './editorConfig'
import AttachmentModel from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import {setupMarkdownRenderer} from '@/helpers/markdownRenderer'
import {findCheckboxesInText} from '@/helpers/checklistFromText'
import {createRandomID} from '@/helpers/randomId'
import BaseButton from '@/components/base/BaseButton.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({
modelValue: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
uploadEnabled: {
type: Boolean,
default: false,
},
uploadCallback: {
type: Function,
},
hasPreview: {
type: Boolean,
default: true,
},
previewIsDefault: {
type: Boolean,
default: true,
},
isEditEnabled: {
default: true,
},
bottomActions: {
type: Array,
default: () => [],
},
emptyText: {
type: String,
default: '',
},
showSave: {
type: Boolean,
default: false,
},
// If a key is passed the editor will go in "edit" mode when the key is pressed.
// Disabled if an empty string is passed.
editShortcut: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'save'])
const text = ref('')
const isEditActive = ref(false)
const isPreviewActive = ref(true)
const showPreviewText = computed(() => isPreviewActive.value && text.value === '' && props.emptyText !== '')
const showEditButton = computed(() => !isEditActive.value && text.value !== '')
const preview = ref('')
const attachmentService = new AttachmentService()
type CacheKey = `${ITask['id']}-${IAttachment['id']}`
const loadedAttachments = ref<{ [key: CacheKey]: string }>({})
const config = ref(createEasyMDEConfig({
placeholder: props.placeholder,
uploadImage: props.uploadEnabled,
imageUploadFunction: props.uploadCallback,
}))
const checkboxId = ref(createRandomID())
const {modelValue} = toRefs(props)
watch(
modelValue,
async (value) => {
text.value = value
await nextTick()
renderPreview()
},
)
watch(
text,
(newVal, oldVal) => {
// Only bubble the new value if it actually changed, but not if the component just got mounted and the text changed from the outside.
if (oldVal === '' && text.value === modelValue.value) {
return
}
bubbleNow()
},
)
onMounted(() => {
if (modelValue.value !== '') {
text.value = modelValue.value
}
if (props.previewIsDefault && props.hasPreview) {
nextTick(() => renderPreview())
return
}
isPreviewActive.value = false
isEditActive.value = true
})
// This gets triggered when only pasting content into the editor.
// A change event would not get generated by that, an input event does.
// Therefore, we're using this handler to catch paste events.
// But because this also gets triggered when typing into the editor, we give
// it a higher timeout to make the timouts cancel each other in that case so
// that in the end, only one change event is triggered to the outside per change.
function handleInput(val: string) {
// Don't bubble if the text is up to date
if (val === text.value) {
return
}
text.value = val
bubbleNow()
}
function bubbleNow() {
emit('update:modelValue', text.value)
}
function replaceAt(str: string, index: number, replacement: string) {
return str.slice(0, index) + replacement + str.slice(index + replacement.length)
}
function findNthIndex(str: string, n: number) {
const checkboxes = findCheckboxesInText(str)
return checkboxes[n]
}
function renderPreview() {
setupMarkdownRenderer(checkboxId.value)
preview.value = DOMPurify.sanitize(marked(text.value, {
mangle: false,
headerIds: false,
}), {ADD_ATTR: ['target']})
// Since the render function is synchronous, we can't do async http requests in it.
// Therefore, we can't resolve the blob url at (markdown) compile time.
// To work around this, we modify the url after rendering it in the vue component.
// We're doing the whole thing in the next tick to ensure the image elements are available in the
// dom tree. If we're calling this right after setting this.preview it could be the images were
// not already made available.
// Some docs at https://stackoverflow.com/q/62865160/10924593
nextTick().then(async () => {
const attachmentImage = document.querySelectorAll<HTMLImageElement>('.attachment-image')
if (attachmentImage) {
Array.from(attachmentImage).forEach(async (img) => {
// The url is something like /tasks/<id>/attachments/<id>
const parts = img.dataset.src?.slice(window.API_URL.length + 1).split('/')
const taskId = Number(parts[1])
const attachmentId = Number(parts[3])
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
if (typeof loadedAttachments.value[cacheKey] !== 'undefined') {
img.src = loadedAttachments.value[cacheKey]
return
}
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
const url = await attachmentService.getBlobUrl(attachment)
img.src = url
loadedAttachments.value[cacheKey] = url
})
}
const textCheckbox = document.querySelectorAll<HTMLInputElement>(`.text-checkbox-${checkboxId.value}`)
if (textCheckbox) {
Array.from(textCheckbox).forEach(check => {
check.removeEventListener('change', handleCheckboxClick)
check.addEventListener('change', handleCheckboxClick)
check.parentElement?.classList.add('has-checkbox')
})
}
})
}
function handleCheckboxClick(e: Event) {
// Find the original markdown checkbox this is targeting
const checked = (e.target as HTMLInputElement).checked
const numMarkdownCheck = Number((e.target as HTMLInputElement).dataset.checkboxNum)
const index = findNthIndex(text.value, numMarkdownCheck)
if (index < 0 || typeof index === 'undefined') {
console.debug('no index found')
return
}
const projectPrefix = text.value.substring(index, index + 1)
console.debug({index, projectPrefix, checked, text: text.value})
text.value = replaceAt(text.value, index, `${projectPrefix} ${checked ? '[x]' : '[ ]'} `)
bubbleNow()
emit('save', text.value)
renderPreview()
}
function toggleEdit() {
isPreviewActive.value = false
isEditActive.value = true
}
function bubbleSaveClick() {
isPreviewActive.value = true
isEditActive.value = false
renderPreview()
bubbleNow()
emit('save', text.value)
}
</script>
<style lang="scss">
@import 'codemirror/lib/codemirror.css';
@import 'highlight.js/scss/base16/equilibrium-gray-light';
.editor {
.clear {
clear: both;
}
.preview.content {
margin-bottom: .5rem;
overflow-wrap: anywhere; // Safari does not understand "break-word" so we put that first to make sure it at least is able to show it somewhat properly there.
overflow-wrap: break-word;
ul li {
input[type="checkbox"] {
margin-right: .5rem;
}
&.has-checkbox {
margin-left: -1.25rem;
list-style: none;
}
}
}
}
.CodeMirror {
padding: .5rem;
border: 1px solid var(--grey-200) !important;
background: var(--white);
&-lines pre {
margin: 0 !important;
}
&-placeholder {
color: var(--grey-400) !important;
font-style: italic;
}
&-cursor {
border-color: var(--grey-700);
}
}
.editor-preview {
padding: 0;
&-side {
padding: .5rem;
}
}
.editor-toolbar {
background: var(--grey-50);
border: 1px solid var(--grey-200);
border-bottom: none;
button {
color: var(--grey-700);
&.active {
background: var(--grey-200);
}
svg {
vertical-align: middle;
&, rect {
width: 20px;
height: 20px;
}
}
&::after {
position: absolute;
top: 24px;
margin-left: -3px;
}
&:hover {
background: var(--grey-200);
border-color: var(--grey-300);
}
}
i.separator {
border-color: var(--grey-200) !important;
}
}
pre.CodeMirror-line {
margin-bottom: 0 !important;
color: var(--grey-700) !important;
}
.cm-header {
font-family: $vikunja-font;
font-weight: 400;
}
ul.actions {
font-size: .8rem;
margin: 0;
li {
display: inline-block;
&::after {
content: '·';
padding: 0 .25rem;
}
&:last-child:after {
content: '';
}
}
&, a {
color: var(--grey-500);
&.done-edit {
color: var(--primary);
}
}
a:hover {
text-decoration: underline;
}
}
.vue-easymde.content {
margin-bottom: 0 !important;
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div class="items">
<template v-if="items.length">
<button
class="item"
:class="{ 'is-selected': index === selectedIndex }"
v-for="(item, index) in items"
:key="index"
@click="selectItem(index)"
>
<icon :icon="item.icon"/>
<div class="description">
<p>{{ item.title }}</p>
<p>{{ item.description }}</p>
</div>
</button>
</template>
<div class="item" v-else>
No result
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
command: {
type: Function,
required: true,
},
},
data() {
return {
selectedIndex: 0,
}
},
watch: {
items() {
this.selectedIndex = 0
},
},
methods: {
onKeyDown({event}) {
if (event.key === 'ArrowUp') {
this.upHandler()
return true
}
if (event.key === 'ArrowDown') {
this.downHandler()
return true
}
if (event.key === 'Enter') {
this.enterHandler()
return true
}
return false
},
upHandler() {
this.selectedIndex = ((this.selectedIndex + this.items.length) - 1) % this.items.length
},
downHandler() {
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
},
enterHandler() {
this.selectItem(this.selectedIndex)
},
selectItem(index) {
const item = this.items[index]
if (item) {
this.command(item)
}
},
},
}
</script>
<style lang="scss" scoped>
.items {
padding: 0.2rem;
position: relative;
border-radius: 0.5rem;
background: var(--white);
color: var(--grey-900);
overflow: hidden;
font-size: 0.9rem;
box-shadow: var(--shadow-md);
}
.item {
display: flex;
align-items: center;
margin: 0;
width: 100%;
text-align: left;
background: transparent;
border-radius: $radius;
border: 0;
padding: 0.2rem 0.4rem;
transition: background-color $transition;
&.is-selected, &:hover {
background: var(--grey-100);
cursor: pointer;
}
> svg {
box-sizing: border-box;
width: 2rem;
height: 2rem;
border: 1px solid var(--grey-300);
padding: .5rem;
margin-right: .5rem;
border-radius: $radius;
color: var(--grey-700);
}
}
.description {
display: flex;
flex-direction: column;
font-size: .9rem;
color: var(--grey-800);
p:last-child {
font-size: .75rem;
color: var(--grey-500);
}
}
</style>

View File

@ -0,0 +1,439 @@
<template>
<div class="editor-toolbar">
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
v-tooltip="$t('input.editor.heading1')"
>
<span class="icon">
<icon :icon="['fa', 'fa-header']"/>
<span class="icon__lower-text">1</span>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
v-tooltip="$t('input.editor.heading2')"
>
<span class="icon">
<icon :icon="['fa', 'fa-header']"/>
<span class="icon__lower-text">2</span>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
v-tooltip="$t('input.editor.heading3')"
>
<span class="icon">
<icon :icon="['fa', 'fa-header']"/>
<span class="icon__lower-text">3</span>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
v-tooltip="$t('input.editor.bold')"
>
<span class="icon">
<icon :icon="['fa', 'fa-bold']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
v-tooltip="$t('input.editor.italic')"
>
<span class="icon">
<icon :icon="['fa', 'fa-italic']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleUnderline().run()"
:class="{ 'is-active': editor.isActive('underline') }"
v-tooltip="$t('input.editor.underline')"
>
<span class="icon">
<icon :icon="['fa', 'fa-underline']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
v-tooltip="$t('input.editor.strikethrough')"
>
<span class="icon">
<icon :icon="['fa', 'fa-strikethrough']"/>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
v-tooltip="$t('input.editor.code')"
>
<span class="icon">
<icon :icon="['fa', 'fa-code']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBlockquote().run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
v-tooltip="$t('input.editor.quote')"
>
<span class="icon">
<icon :icon="['fa', 'fa-quote-right']"/>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
v-tooltip="$t('input.editor.bulletList')"
>
<span class="icon">
<icon :icon="['fa', 'fa-list-ol']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
v-tooltip="$t('input.editor.orderedList')"
>
<span class="icon">
<icon :icon="['fa', 'fa-list-ul']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleTaskList().run()"
:class="{ 'is-active': editor.isActive('taskList') }"
v-tooltip="$t('input.editor.taskList')"
>
<span class="icon">
<icon icon="fa-list-check"/>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="openImagePicker"
v-tooltip="$t('input.editor.image')"
>
<span class="icon">
<icon icon="fa-image"/>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="setLink"
:class="{ 'is-active': editor.isActive('link') }"
title="set link"
v-tooltip="$t('input.editor.link')"
>
<span class="icon">
<icon :icon="['fa', 'fa-link']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().setParagraph().run()"
:class="{ 'is-active': editor.isActive('paragraph') }"
title="paragraph"
v-tooltip="$t('input.editor.text')"
>
<span class="icon">
<icon :icon="['fa', 'fa-paragraph']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().setHorizontalRule().run()"
v-tooltip="$t('input.editor.horizontalRule')"
>
<span class="icon">
<icon :icon="['fa', 'fa-ruler-horizontal']"/>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().undo().run()"
v-tooltip="$t('input.editor.undo')"
>
<span class="icon">
<icon :icon="['fa', 'fa-undo']"/>
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().redo().run()"
v-tooltip="$t('input.editor.redo')"
>
<span class="icon">
<icon :icon="['fa', 'fa-redo']"/>
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<!-- table -->
<BaseButton
class="editor-toolbar__button"
@click="toggleTableMode"
:class="{ 'is-active': editor.isActive('table') }"
v-tooltip="$t('input.editor.table.title')"
>
<span class="icon">
<icon :icon="['fa', 'fa-table']"/>
</span>
</BaseButton>
<div v-if="tableMode" class="editor-toolbar__table-buttons">
<BaseButton
class="editor-toolbar__button"
@click="
editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run()
"
>
{{ $t('input.editor.table.insert') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addColumnBefore().run()"
:disabled="!editor.can().addColumnBefore"
>
{{ $t('input.editor.table.addColumnBefore') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addColumnAfter().run()"
:disabled="!editor.can().addColumnAfter"
>
{{ $t('input.editor.table.addColumnAfter') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteColumn().run()"
:disabled="!editor.can().deleteColumn"
>
{{ $t('input.editor.table.deleteColumn') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addRowBefore().run()"
:disabled="!editor.can().addRowBefore"
>
{{ $t('input.editor.table.addRowBefore') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addRowAfter().run()"
:disabled="!editor.can().addRowAfter"
>
{{ $t('input.editor.table.addRowAfter') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteRow().run()"
:disabled="!editor.can().deleteRow"
>
{{ $t('input.editor.table.deleteRow') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteTable().run()"
:disabled="!editor.can().deleteTable"
>
{{ $t('input.editor.table.deleteTable') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().mergeCells().run()"
:disabled="!editor.can().mergeCells"
>
{{ $t('input.editor.table.mergeCells') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().splitCell().run()"
:disabled="!editor.can().splitCell"
>
{{ $t('input.editor.table.splitCell') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderColumn().run()"
:disabled="!editor.can().toggleHeaderColumn"
>
{{ $t('input.editor.table.toggleHeaderColumn') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderRow().run()"
:disabled="!editor.can().toggleHeaderRow"
>
{{ $t('input.editor.table.toggleHeaderRow') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderCell().run()"
:disabled="!editor.can().toggleHeaderCell"
>
{{ $t('input.editor.table.toggleHeaderCell') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().mergeOrSplit().run()"
:disabled="!editor.can().mergeOrSplit"
>
{{ $t('input.editor.table.mergeOrSplit') }}
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().fixTables().run()"
:disabled="!editor.can().fixTables"
>
{{ $t('input.editor.table.fixTables') }}
</BaseButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import {Editor} from '@tiptap/vue-3'
import BaseButton from '@/components/base/BaseButton.vue'
const {
editor = null,
} = defineProps<{
editor: Editor,
}>()
const tableMode = ref(false)
function toggleTableMode() {
tableMode.value = !tableMode.value
}
function openImagePicker() {
document.getElementById('tiptap__image-upload').click()
}
function setLink() {
const previousUrl = editor.getAttributes('link').href
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null) {
return
}
// empty
if (url === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
editor
.chain()
.focus()
.extendMarkRange('link')
.setLink({href: url, target: '_blank'})
.run()
}
</script>
<style lang="scss" scoped>
.editor-toolbar {
background: var(--white);
border: 1px solid var(--grey-200);
user-select: none;
padding: .5rem;
border-radius: $radius;
display: flex;
flex-wrap: wrap;
> * + * {
border-left: 1px solid var(--grey-200);
margin-left: 6px;
padding-left: 6px;
}
}
.editor-toolbar__button {
min-width: 2rem;
height: 2rem;
border-radius: $radius;
border: 1px solid transparent;
color: var(--grey-700);
transition: all $transition;
background: transparent;
margin-right: .25rem;
&:hover {
background: var(--grey-100);
border-color: var(--grey-200);
}
.icon {
position: relative;
.icon__lower-text {
font-size: .75rem;
position: absolute;
bottom: -3px;
right: -2px;
font-weight: bold;
}
}
}
.editor-toolbar__table-buttons {
margin-top: .5rem;
> .editor-toolbar__button {
margin-right: .5rem;
margin-bottom: .5rem;
padding: 0 .25rem;
border: 1px solid var(--grey-400);
font-size: .75rem;
height: 1.5rem;
}
}
</style>

View File

@ -0,0 +1,842 @@
<template>
<div class="tiptap">
<EditorToolbar
v-if="editor && isEditing"
:editor="editor"
:upload-callback="uploadCallback"
/>
<BubbleMenu
v-if="editor && isEditing"
:editor="editor"
class="editor-bubble__wrapper"
>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
v-tooltip="$t('input.editor.bold')"
>
<icon :icon="['fa', 'fa-bold']"/>
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
v-tooltip="$t('input.editor.italic')"
>
<icon :icon="['fa', 'fa-italic']"/>
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleUnderline().run()"
:class="{ 'is-active': editor.isActive('underline') }"
v-tooltip="$t('input.editor.underline')"
>
<icon :icon="['fa', 'fa-underline']"/>
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
v-tooltip="$t('input.editor.strikethrough')"
>
<icon :icon="['fa', 'fa-strikethrough']"/>
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="editor.chain().focus().toggleCode().run()"
:class="{ 'is-active': editor.isActive('code') }"
v-tooltip="$t('input.editor.code')"
>
<icon :icon="['fa', 'fa-code']"/>
</BaseButton>
<BaseButton
class="editor-bubble__button"
@click="setLink"
:class="{ 'is-active': editor.isActive('link') }"
v-tooltip="$t('input.editor.link')"
>
<icon :icon="['fa', 'fa-link']"/>
</BaseButton>
</BubbleMenu>
<editor-content
class="tiptap__editor"
:class="{'tiptap__editor-is-empty': isEmpty, 'tiptap__editor-is-edit-enabled': isEditing}"
:editor="editor"
/>
<input
v-if="isEditing"
type="file"
id="tiptap__image-upload"
class="is-hidden"
ref="uploadInputRef"
@change="addImage"
/>
<ul class="tiptap__editor-actions d-print-none" v-if="bottomActions.length === 0 && !isEditing">
<li>
<BaseButton
@click="setEdit"
class="done-edit">
{{ $t('input.editor.edit') }}
</BaseButton>
</li>
</ul>
<ul class="tiptap__editor-actions d-print-none" v-if="bottomActions.length > 0">
<li v-if="isEditing && showSave">
<BaseButton
@click="bubbleSave"
class="done-edit">
{{ $t('misc.save') }}
</BaseButton>
</li>
<li v-if="!isEditing">
<BaseButton
@click="setEdit"
class="done-edit">
{{ $t('input.editor.edit') }}
</BaseButton>
</li>
<li v-for="(action, k) in bottomActions" :key="k">
<BaseButton @click="action.action">{{ action.title }}</BaseButton>
</li>
</ul>
<x-button
v-else-if="isEditing && showSave"
class="mt-4"
@click="bubbleSave"
variant="secondary"
:shadow="false"
v-cy="'saveEditor'"
>
{{ $t('misc.save') }}
</x-button>
</div>
</template>
<script setup lang="ts">
import {ref, watch, onBeforeUnmount, nextTick, onMounted, computed} from 'vue'
import {refDebounced} from '@vueuse/core'
import EditorToolbar from './EditorToolbar.vue'
import Link from '@tiptap/extension-link'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import Typography from '@tiptap/extension-typography'
import Image from '@tiptap/extension-image'
import Underline from '@tiptap/extension-underline'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import {Blockquote} from '@tiptap/extension-blockquote'
import {Bold} from '@tiptap/extension-bold'
import {BulletList} from '@tiptap/extension-bullet-list'
import {Code} from '@tiptap/extension-code'
import {Document} from '@tiptap/extension-document'
import {Dropcursor} from '@tiptap/extension-dropcursor'
import {Gapcursor} from '@tiptap/extension-gapcursor'
import {HardBreak} from '@tiptap/extension-hard-break'
import {Heading} from '@tiptap/extension-heading'
import {History} from '@tiptap/extension-history'
import {HorizontalRule} from '@tiptap/extension-horizontal-rule'
import {Italic} from '@tiptap/extension-italic'
import {ListItem} from '@tiptap/extension-list-item'
import {OrderedList} from '@tiptap/extension-ordered-list'
import {Paragraph} from '@tiptap/extension-paragraph'
import {Strike} from '@tiptap/extension-strike'
import {Text} from '@tiptap/extension-text'
import {BubbleMenu, EditorContent, useEditor} from '@tiptap/vue-3'
import {Node} from '@tiptap/pm/model'
import Commands from './commands'
import suggestionSetup from './suggestion'
import {lowlight} from 'lowlight'
import type {BottomAction, UploadCallback} from './types'
import type {ITask} from '@/modelTypes/ITask'
import type {IAttachment} from '@/modelTypes/IAttachment'
import AttachmentModel from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue'
import XButton from '@/components/input/button.vue'
import {Placeholder} from '@tiptap/extension-placeholder'
import {eventToHotkeyString} from '@github/hotkey'
import {mergeAttributes} from '@tiptap/core'
import {createRandomID} from '@/helpers/randomId'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const {t} = useI18n()
const CustomTableCell = TableCell.extend({
addAttributes() {
return {
// extend the existing attributes
...this.parent?.(),
// and add a new one
backgroundColor: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute('data-background-color'),
renderHTML: (attributes) => {
return {
'data-background-color': attributes.backgroundColor,
style: `background-color: ${attributes.backgroundColor}`,
}
},
},
}
},
})
type CacheKey = `${ITask['id']}-${IAttachment['id']}`
const loadedAttachments = ref<{ [key: CacheKey]: string }>({})
const CustomImage = Image.extend({
renderHTML({HTMLAttributes}) {
if (HTMLAttributes.src?.startsWith(window.API_URL)) {
const id = 'tiptap-image-' + createRandomID()
nextTick(async () => {
const img = document.getElementById(id)
if (!img) return
// The url is something like /tasks/<id>/attachments/<id>
const parts = img.dataset?.src.slice(window.API_URL.length + 1).split('/')
const taskId = Number(parts[1])
const attachmentId = Number(parts[3])
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
if (typeof loadedAttachments.value[cacheKey] === 'undefined') {
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
const attachmentService = new AttachmentService()
const url = await attachmentService.getBlobUrl(attachment)
loadedAttachments.value[cacheKey] = url
}
img.src = loadedAttachments.value[cacheKey]
})
return ['img', mergeAttributes(this.options.HTMLAttributes, {
'data-src': HTMLAttributes.src,
src: '#',
alt: HTMLAttributes.alt,
title: HTMLAttributes.title,
id,
})]
}
return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
},
})
type Mode = 'edit' | 'preview'
const {
modelValue,
uploadCallback,
isEditEnabled = true,
bottomActions = [],
showSave = false,
placeholder = '',
editShortcut = '',
initialMode = 'edit',
} = defineProps<{
modelValue: string,
uploadCallback?: UploadCallback,
isEditEnabled?: boolean,
bottomActions?: BottomAction[],
showSave?: boolean,
placeholder?: string,
editShortcut?: string,
initialMode?: Mode,
}>()
const emit = defineEmits(['update:modelValue', 'save'])
const inputHTML = ref('')
const isEmpty = computed(() => isEditorContentEmpty(inputHTML.value))
const internalMode = ref<Mode>(initialMode)
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const editor = useEditor({
content: modelValue,
editable: isEditing.value,
extensions: [
// Starterkit:
Blockquote,
Bold,
BulletList,
Code,
CodeBlockLowlight.configure({
lowlight,
}),
Document,
Dropcursor,
Gapcursor,
HardBreak.extend({
addKeyboardShortcuts() {
return {
'Mod-Enter': () => {
bubbleSave()
},
}
},
}),
Heading,
History,
HorizontalRule,
Italic,
ListItem,
OrderedList,
Paragraph,
Strike,
Text,
Placeholder.configure({
placeholder: ({editor}) => {
if (!isEditing.value) {
return ''
}
if (editor.getText() !== '' && !editor.isFocused) {
return ''
}
return placeholder !== ''
? placeholder
: t('input.editor.placeholder')
},
}),
Typography,
Underline,
Link.configure({
openOnClick: true,
validate: (href: string) => /^https?:\/\//.test(href),
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
// Custom TableCell with backgroundColor attribute
CustomTableCell,
CustomImage,
TaskList,
TaskItem.configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (isEditEnabled) {
node.attrs.checked = checked
inputHTML.value = editor.value?.getHTML()
bubbleSave()
return true
}
return false
},
}),
Commands.configure({
suggestion: suggestionSetup(t),
}),
BubbleMenu,
],
onUpdate: () => {
inputHTML.value = editor.value!.getHTML()
},
})
watch(
() => modelValue,
value => {
inputHTML.value = value
if (!editor?.value) return
if (editor.value.getHTML() === value) {
return
}
editor.value.commands.setContent(value, false)
},
)
const debouncedInputHTML = refDebounced(inputHTML, 1000)
watch(debouncedInputHTML, () => bubbleNow())
function bubbleNow() {
emit('update:modelValue', inputHTML.value)
}
function bubbleSave() {
bubbleNow()
emit('save', inputHTML.value)
if (isEditing.value) {
internalMode.value = 'preview'
}
}
function setEdit() {
internalMode.value = 'edit'
editor.value?.commands.focus()
}
watch(
() => isEditing.value,
() => {
editor.value?.setEditable(isEditing.value)
},
)
onBeforeUnmount(() => editor.value?.destroy())
const uploadInputRef = ref<HTMLInputElement | null>(null)
function uploadAndInsertFiles(files: File[] | FileList) {
uploadCallback(files).then(urls => {
urls?.forEach(url => {
editor.value
?.chain()
.focus()
.setImage({src: url})
.run()
})
bubbleSave()
})
}
function addImage() {
if (typeof uploadCallback !== 'undefined') {
const files = uploadInputRef.value?.files
if (!files || files.length === 0) {
return
}
uploadAndInsertFiles(files)
return
}
const url = window.prompt('URL')
if (url) {
editor.value?.chain().focus().setImage({src: url}).run()
bubbleSave()
}
}
function setLink() {
const previousUrl = editor.value?.getAttributes('link').href
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null) {
return
}
// empty
if (url === '') {
editor.value
?.chain()
.focus()
.extendMarkRange('link')
.unsetLink()
.run()
return
}
// update link
editor.value
?.chain()
.focus()
.extendMarkRange('link')
.setLink({href: url, target: '_blank'})
.run()
}
onMounted(() => {
internalMode.value = initialMode
document.addEventListener('paste', handleImagePaste)
if (editShortcut !== '') {
document.addEventListener('keydown', setFocusToEditor)
}
})
onBeforeUnmount(() => {
document.removeEventListener('paste', handleImagePaste)
if (editShortcut !== '') {
document.removeEventListener('keydown', setFocusToEditor)
}
})
function handleImagePaste(event) {
event.preventDefault()
event?.clipboardData?.items?.forEach(i => {
if (i.kind === 'file' && i.type.startsWith('image/')) {
uploadAndInsertFiles([i.getAsFile()])
}
})
}
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
function setFocusToEditor(event) {
const hotkeyString = eventToHotkeyString(event)
if (!hotkeyString) return
if (hotkeyString !== editShortcut ||
event.target.tagName.toLowerCase() === 'input' ||
event.target.tagName.toLowerCase() === 'textarea' ||
event.target.contentEditable === 'true') {
return
}
event.preventDefault()
if (initialMode === 'preview' && isEditEnabled && !isEditing.value) {
internalMode.value = 'edit'
}
editor.value?.commands.focus()
}
</script>
<style lang="scss">
.tiptap__editor {
&.tiptap__editor-is-edit-enabled {
min-height: 10rem;
}
transition: box-shadow $transition;
border-radius: $radius;
&:focus-within, &:focus {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
}
}
.tiptap p.is-empty::before {
content: attr(data-placeholder);
color: var(--grey-400);
pointer-events: none;
height: 0;
float: left;
}
// Basic editor styles
.ProseMirror {
padding: .5rem;
&:focus-within, &:focus {
box-shadow: none;
}
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
a {
color: #68cef8;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0d0d0d;
color: #fff;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
pre {
background: #0d0d0d;
color: #fff;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
img {
max-width: 100%;
height: auto;
&.ProseMirror-selectednode {
outline: 3px solid #68cef8;
}
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0d0d0d, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0d0d0d, 0.1);
margin: 2rem 0;
}
}
.ProseMirror {
/* Table-specific styling */
table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
td,
th {
min-width: 1em;
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box;
position: relative;
> * {
margin-bottom: 0;
}
}
th {
font-weight: bold;
text-align: left;
background-color: #f1f3f5;
}
.selectedCell:after {
z-index: 2;
position: absolute;
content: '';
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none;
}
.column-resize-handle {
position: absolute;
right: -2px;
top: 0;
bottom: -2px;
width: 4px;
background-color: #adf;
pointer-events: none;
}
p {
margin: 0;
}
}
// Lists
ul {
margin-left: .5rem;
margin-top: 0 !important;
li {
margin-top: 0;
}
p {
margin-bottom: 0 !important;
}
}
}
.tableWrapper {
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
// tasklist
ul[data-type='taskList'] {
list-style: none;
padding: 0;
margin-left: 0;
li {
display: flex;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
}
input[type='checkbox'] {
cursor: pointer;
}
}
.editor-bubble__wrapper {
background: var(--white);
border-radius: $radius;
border: 1px solid var(--grey-200);
box-shadow: var(--shadow-md);
display: flex;
overflow: hidden;
}
.editor-bubble__button {
color: var(--grey-700);
transition: all $transition;
background: transparent;
svg {
box-sizing: border-box;
display: block;
width: 1rem;
height: 1rem;
padding: .5rem;
margin: 0;
}
&:hover {
background: var(--grey-200);
}
}
ul.tiptap__editor-actions {
font-size: .8rem;
margin: 0;
li {
display: inline-block;
&::after {
content: '·';
padding: 0 .25rem;
}
&:last-child:after {
content: '';
}
}
&, a {
color: var(--grey-500);
&.done-edit {
color: var(--primary);
}
}
a:hover {
text-decoration: underline;
}
}
</style>

View File

@ -0,0 +1,28 @@
import {Extension} from '@tiptap/core'
import Suggestion from '@tiptap/suggestion'
// Copied and adjusted from https://github.com/ueberdosis/tiptap/tree/252acb32d27a0f9af14813eeed83d8a50059a43a/demos/src/Experiments/Commands/Vue
export default Extension.create({
name: 'slash-menu-commands',
addOptions() {
return {
suggestion: {
char: '/',
command: ({editor, range, props}) => {
props.command({editor, range})
},
},
}
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion,
}),
]
},
})

View File

@ -0,0 +1,214 @@
import {VueRenderer} from '@tiptap/vue-3'
import tippy from 'tippy.js'
import CommandsList from './CommandsList.vue'
export default function suggestionSetup(t) {
return {
items: ({query}: { query: string }) => {
return [
{
title: t('input.editor.text'),
description: t('input.editor.textTooltip'),
icon: 'fa-font',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('paragraph', {level: 1})
.run()
},
},
{
title: t('input.editor.heading1'),
description: t('input.editor.heading1Tooltip'),
icon: 'fa-header',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', {level: 1})
.run()
},
},
{
title: t('input.editor.heading2'),
description: t('input.editor.heading2Tooltip'),
icon: 'fa-header',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', {level: 2})
.run()
},
},
{
title: t('input.editor.heading3'),
description: t('input.editor.heading3Tooltip'),
icon: 'fa-header',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', {level: 2})
.run()
},
},
{
title: t('input.editor.bulletList'),
description: t('input.editor.bulletListTooltip'),
icon: 'fa-list-ul',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleBulletList()
.run()
},
},
{
title: t('input.editor.orderedList'),
description: t('input.editor.orderedListTooltip'),
icon: 'fa-list-ol',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleOrderedList()
.run()
},
},
{
title: t('input.editor.taskList'),
description: t('input.editor.taskListTooltip'),
icon: 'fa-list-check',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleTaskList()
.run()
},
},
{
title: t('input.editor.quote'),
description: t('input.editor.quoteTooltip'),
icon: 'fa-quote-right',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleBlockquote()
.run()
},
},
{
title: t('input.editor.code'),
description: t('input.editor.codeTooltip'),
icon: 'fa-code',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleCodeBlock()
.run()
},
},
{
title: t('input.editor.image'),
description: t('input.editor.imageTooltip'),
icon: 'fa-image',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.run()
document.getElementById('tiptap__image-upload').click()
},
},
{
title: t('input.editor.horizontalRule'),
description: t('input.editor.horizontalRuleTooltip'),
icon: 'fa-ruler-horizontal',
command: ({editor, range}) => {
editor
.chain()
.focus()
.deleteRange(range)
.setHorizontalRule()
.run()
},
},
].filter(item => item.title.toLowerCase().startsWith(query.toLowerCase()))
},
render: () => {
let component: VueRenderer
let popup
return {
onStart: props => {
component = new VueRenderer(CommandsList, {
// using vue 2:
// parent: this,
// propsData: props,
props,
editor: props.editor,
})
if (!props.clientRect) {
return
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
})
},
onUpdate(props) {
component.updateProps(props)
if (!props.clientRect) {
return
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
})
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide()
return true
}
return component.ref?.onKeyDown(props)
},
onExit() {
popup[0].destroy()
component.destroy()
},
}
},
}
}

View File

@ -0,0 +1,6 @@
export type UploadCallback = (files: File[] | FileList) => Promise<string[]>
export interface BottomAction {
title: string
action: () => void,
}

View File

@ -1,135 +0,0 @@
import EasyMDE from 'easymde'
import {i18n} from '@/i18n'
export function createEasyMDEConfig({ placeholder, uploadImage, imageUploadFunction }) {
return {
autoDownloadFontAwesome: false,
spellChecker: false,
placeholder,
uploadImage,
imageUploadFunction,
minHeight: '150px',
sideBySideFullscreen: false,
toolbar: [
{
name: 'heading-1',
action: EasyMDE.toggleHeading1,
title: i18n.global.t('input.editor.heading1'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
{
name: 'heading-2',
action: EasyMDE.toggleHeading2,
title: i18n.global.t('input.editor.heading2'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
{
name: 'heading-3',
action: EasyMDE.toggleHeading3,
title: i18n.global.t('input.editor.heading3'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
{
name: 'heading-smaller',
action: EasyMDE.toggleHeadingSmaller,
title: i18n.global.t('input.editor.headingSmaller'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
{
name: 'heading-bigger',
action: EasyMDE.toggleHeadingBigger,
title: i18n.global.t('input.editor.headingBigger'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
'|',
{
name: 'bold',
action: EasyMDE.toggleBold,
title: i18n.global.t('input.editor.bold'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3H6.5H15.25C18.15 3 20.5 5.36 20.5 8.25C20.5 9.8 19.81 11.19 18.73 12.15C20.37 13.04 21.5 14.76 21.5 16.75C21.5 19.64 19.15 22 16.25 22H6.5H3.5C2.95 22 2.5 21.55 2.5 21C2.5 20.45 2.95 20 3.5 20H5.5V5H3.5C2.95 5 2.5 4.55 2.5 4C2.5 3.45 2.95 3 3.5 3ZM7.5 20H16.25C18.04 20 19.5 18.54 19.5 16.75C19.5 14.96 18.04 13.5 16.25 13.5H7.5V20ZM7.5 11.5H15.25C17.04 11.5 18.5 10.04 18.5 8.25C18.5 6.46 17.04 5 15.25 5H7.5V11.5Z"/></svg>',
},
{
name: 'italic',
action: EasyMDE.toggleItalic,
title: i18n.global.t('input.editor.italic'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M14.0967 4.2H17.0001C17.3301 4.2 17.6001 3.93 17.6001 3.6C17.6001 3.27 17.3301 3 17.0001 3H10.2001C9.8701 3 9.6001 3.27 9.6001 3.6C9.6001 3.93 9.8701 4.2 10.2001 4.2H12.8748L9.90335 19.8H6.9999C6.6699 19.8 6.3999 20.07 6.3999 20.4C6.3999 20.73 6.6699 21 6.9999 21H13.7999C14.1299 21 14.3999 20.73 14.3999 20.4C14.3999 20.07 14.1299 19.8 13.7999 19.8H11.1253L14.0967 4.2Z"/></svg>',
},
{
name: 'strikethrough',
action: EasyMDE.toggleStrikethrough,
title: i18n.global.t('input.editor.strikethrough'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M18.25 7.17005C18.25 7.50005 17.98 7.77005 17.65 7.77005C17.32 7.77005 17.05 7.50005 17.05 7.17005V5.96005C15.97 5.12005 14.17 4.56005 12.79 4.31005C11.1 4.00005 9.51 4.30005 8.41 5.12005C7.2 6.03005 6.67 7.67005 7.19 8.88005C7.56 9.73005 8.37 10.31 8.98 10.64C9.57215 10.9644 10.1961 11.2013 10.8465 11.3999H20.4C20.73 11.3999 21 11.6699 21 11.9999C21 12.3299 20.73 12.5999 20.4 12.5999H15.3012C16.6583 13.0929 17.5255 13.7765 17.95 14.69C18.73 16.36 17.74 18.33 16.36 19.41C15.05 20.4401 13.35 21 11.54 21H11.16C9.78 20.9401 8.34 20.5301 6.95 19.85V20.3601C6.95 20.6901 6.68 20.96 6.35 20.96C6.02 20.96 5.75 20.6901 5.75 20.3601V17.36C5.75 17.03 6.02 16.76 6.35 16.76C6.68 16.76 6.95 17.03 6.95 17.36V18.5C8.35 19.2801 9.81 19.74 11.21 19.8C12.86 19.89 14.46 19.39 15.62 18.48C16.6 17.71 17.37 16.3 16.86 15.21C16.55 14.54 15.8 14.0201 14.58 13.63C13.9711 13.4331 13.3222 13.2762 12.6906 13.1235C12.6168 13.1056 12.5432 13.0878 12.47 13.07C12.4313 13.0607 12.3925 13.0514 12.3537 13.0421C11.7861 12.9055 11.2108 12.767 10.6413 12.5999H3.6C3.27 12.5999 3 12.3299 3 11.9999C3 11.6699 3.27 11.3999 3.6 11.3999H7.90288C7.04984 10.8343 6.42752 10.1363 6.09 9.36005C5.34 7.63005 6.03 5.40005 7.69 4.16005C9.05 3.15005 10.99 2.77005 13 3.13005C13.64 3.25005 15.53 3.66005 17.05 4.53005V4.17005C17.05 3.84005 17.32 3.57005 17.65 3.57005C17.98 3.57005 18.25 3.84005 18.25 4.17005V7.17005Z"/></svg>',
},
{
name: 'code',
action: EasyMDE.toggleCodeBlock,
title: i18n.global.t('input.editor.code'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M8.57 20.9601C8.64 20.9901 8.71 21.0001 8.78 21.0001C9.02 21.0001 9.24 20.8501 9.34 20.6101L15.79 3.81005C15.9 3.50005 15.75 3.15005 15.44 3.03005C15.14 2.92005 14.79 3.07005 14.67 3.38005L8.22 20.1801C8.11 20.4901 8.26 20.8401 8.57 20.9601ZM7.00007 18.0001C6.85007 18.0001 6.69007 17.9401 6.58007 17.8201L1.18007 12.4201C0.950068 12.1901 0.950068 11.8101 1.18007 11.5701L6.58007 6.17006C6.81007 5.94006 7.19007 5.94006 7.43007 6.17006C7.66007 6.40006 7.66007 6.78006 7.43007 7.02006L2.45007 12.0001L7.43007 16.9801C7.66007 17.2101 7.66007 17.5901 7.43007 17.8301C7.31007 17.9401 7.15007 18.0001 7.00007 18.0001ZM17 18.0001C16.85 18.0001 16.69 17.9401 16.58 17.8201C16.35 17.5901 16.35 17.2101 16.58 16.9701L21.55 12.0001L16.57 7.02006C16.34 6.79006 16.34 6.41006 16.57 6.17006C16.81 5.94006 17.19 5.94006 17.42 6.17006L22.82 11.5701C23.05 11.8001 23.05 12.1801 22.82 12.4201L17.42 17.8201C17.31 17.9401 17.15 18.0001 17 18.0001Z"/></svg>',
},
{
name: 'quote',
action: EasyMDE.toggleBlockquote,
title: i18n.global.t('input.editor.quote'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M19.373 5.16357H5.62695C4.79102 5.16357 4.11133 5.84326 4.11133 6.6792V16.2095C4.11133 17.0464 4.79102 17.7261 5.62695 17.7261H6.8877V21.1245C6.8877 21.3667 7.0332 21.5854 7.25684 21.6782C7.33203 21.7095 7.41016 21.7241 7.4873 21.7241C7.64258 21.7241 7.7959 21.6636 7.91113 21.5493L11.748 17.7261H19.373C20.209 17.7261 20.8887 17.0464 20.8887 16.2095V6.6792C20.8887 5.84326 20.209 5.16357 19.373 5.16357ZM19.6895 16.2095C19.6895 16.3843 19.5469 16.5269 19.373 16.5269H11.5C11.3408 16.5269 11.1895 16.5894 11.0762 16.7017L8.08691 19.6802V17.1265C8.08691 16.7954 7.81836 16.5269 7.4873 16.5269H5.62695C5.45312 16.5269 5.31055 16.3843 5.31055 16.2095V6.6792C5.31055 6.50537 5.45312 6.36279 5.62695 6.36279H19.373C19.5469 6.36279 19.6895 6.50537 19.6895 6.6792V16.2095ZM10.3431 8.45264C9.46326 8.45264 8.75 9.16589 8.75 10.0458C8.75 10.9257 9.46326 11.639 10.3431 11.639C10.4775 11.639 10.6058 11.6173 10.7305 11.5861V11.6195C10.7305 12.1322 10.3135 12.5492 9.75586 12.5492C9.4248 12.5492 9.17871 12.8177 9.17871 13.1488C9.17871 13.4799 9.46973 13.7484 9.80078 13.7484C10.9746 13.7484 11.9297 12.7933 11.9297 11.6195V10.1176L11.9294 10.1165L11.9292 10.1155C11.9297 10.1049 11.9312 10.0946 11.9326 10.0843L11.9326 10.0843C11.9345 10.0716 11.9363 10.059 11.9363 10.0458C11.9363 9.16589 11.223 8.45264 10.3431 8.45264ZM13.0637 10.0458C13.0637 9.16589 13.7771 8.45264 14.657 8.45264C15.5369 8.45264 16.2501 9.16589 16.2501 10.0458C16.2501 10.0584 16.2484 10.0706 16.2466 10.0828C16.2452 10.0929 16.2437 10.103 16.2433 10.1134C16.2433 10.1149 16.2441 10.1161 16.2441 10.1176V11.6195C16.2441 12.7933 15.2891 13.7484 14.1152 13.7484C13.7842 13.7484 13.4922 13.4799 13.4922 13.1488C13.4922 12.8177 13.7383 12.5492 14.0693 12.5492C14.6279 12.5492 15.0449 12.1322 15.0449 11.6195V11.5858C14.9202 11.6173 14.7915 11.639 14.657 11.639C13.7771 11.639 13.0637 10.9257 13.0637 10.0458Z"/></svg>',
},
{
name: 'unordered-list',
action: EasyMDE.toggleUnorderedList,
title: i18n.global.t('input.editor.unorderedList'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M6.5281 3.7002H3.5281C3.1981 3.7002 2.9281 3.9702 2.9281 4.3002V7.3002C2.9281 7.6302 3.1981 7.9002 3.5281 7.9002H6.5281C6.8581 7.9002 7.1281 7.6302 7.1281 7.3002V4.3002C7.1281 3.9702 6.8581 3.7002 6.5281 3.7002ZM5.9281 6.7002H4.1281V4.9002H5.9281V6.7002ZM3.5281 9.90015H6.5281C6.8581 9.90015 7.1281 10.1701 7.1281 10.5001V13.5001C7.1281 13.8301 6.8581 14.1001 6.5281 14.1001H3.5281C3.1981 14.1001 2.9281 13.8301 2.9281 13.5001V10.5001C2.9281 10.1701 3.1981 9.90015 3.5281 9.90015ZM4.1281 12.9001H5.9281V11.1001H4.1281V12.9001ZM3.5281 16.1001H6.5281C6.8581 16.1001 7.1281 16.3701 7.1281 16.7001V19.7001C7.1281 20.0301 6.8581 20.3001 6.5281 20.3001H3.5281C3.1981 20.3001 2.9281 20.0301 2.9281 19.7001V16.7001C2.9281 16.3701 3.1981 16.1001 3.5281 16.1001ZM4.1281 19.1001H5.9281V17.3001H4.1281V19.1001ZM9.72817 6.4002H20.7282C21.0582 6.4002 21.3282 6.1302 21.3282 5.8002C21.3282 5.4702 21.0582 5.2002 20.7282 5.2002H9.72817C9.39817 5.2002 9.12817 5.4702 9.12817 5.8002C9.12817 6.1302 9.39817 6.4002 9.72817 6.4002ZM9.72817 11.4001H20.7282C21.0582 11.4001 21.3282 11.6701 21.3282 12.0001C21.3282 12.3301 21.0582 12.6001 20.7282 12.6001H9.72817C9.39817 12.6001 9.12817 12.3301 9.12817 12.0001C9.12817 11.6701 9.39817 11.4001 9.72817 11.4001ZM9.72817 17.6001H20.7282C21.0582 17.6001 21.3282 17.8701 21.3282 18.2001C21.3282 18.5301 21.0582 18.8001 20.7282 18.8001H9.72817C9.39817 18.8001 9.12817 18.5301 9.12817 18.2001C9.12817 17.8701 9.39817 17.6001 9.72817 17.6001Z"/></svg>',
},
{
name: 'ordered-list',
action: EasyMDE.toggleOrderedList,
title: i18n.global.t('input.editor.orderedList'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M4.19494 8.29994H5.99494C6.26494 8.29994 6.49494 8.07995 6.49494 7.79994C6.49494 7.51995 6.27494 7.29994 5.99494 7.29994H5.59494V3.79994C5.59494 3.62994 5.50494 3.46994 5.36494 3.37994C5.22494 3.28994 5.04494 3.26994 4.89494 3.33994L3.89494 3.76994C3.64494 3.87994 3.52494 4.17994 3.63494 4.42994C3.74494 4.67994 4.03494 4.79994 4.29494 4.68994L4.59494 4.55994V7.29994H4.19494C3.91494 7.29994 3.69494 7.51995 3.69494 7.79994C3.69494 8.07995 3.91494 8.29994 4.19494 8.29994ZM20.195 6.39995H9.19497C8.86497 6.39995 8.59497 6.12995 8.59497 5.79995C8.59497 5.46995 8.86497 5.19995 9.19497 5.19995H20.195C20.525 5.19995 20.795 5.46995 20.795 5.79995C20.795 6.12995 20.525 6.39995 20.195 6.39995ZM3.78486 14.36H6.37486C6.65486 14.36 6.87486 14.14 6.87486 13.86C6.87486 13.58 6.65486 13.36 6.37486 13.36H4.88486C5.00486 13.23 5.12486 13.09 5.23486 12.95C5.26626 12.9151 5.29645 12.8802 5.32626 12.8458L5.32629 12.8457C5.38192 12.7814 5.43627 12.7186 5.49486 12.66C5.73486 12.4 5.98486 12.12 6.17486 11.79C6.47486 11.25 6.41486 10.63 6.01486 10.17C5.57486 9.66 4.86486 9.5 4.24486 9.74C3.74486 9.95 3.39486 10.35 3.22486 10.91C3.14486 11.18 3.29486 11.46 3.56486 11.54C3.82486 11.61 4.10486 11.46 4.18486 11.2C4.29486 10.85 4.48486 10.73 4.62486 10.67C4.88486 10.57 5.13486 10.68 5.26486 10.82C5.38486 10.96 5.40486 11.12 5.30486 11.29C5.17595 11.5202 4.99618 11.7165 4.80458 11.9257L4.75486 11.98C4.67298 12.0801 4.58283 12.1801 4.49946 12.2727L4.49945 12.2727L4.47486 12.3C4.12486 12.72 3.76486 13.13 3.40486 13.53C3.27486 13.68 3.23486 13.9 3.32486 14.07C3.41486 14.24 3.58486 14.36 3.78486 14.36ZM3.68486 20.3699C4.04486 20.5899 4.46486 20.6999 4.87486 20.6999C5.13486 20.6999 5.38486 20.6499 5.61486 20.5499C6.31486 20.2799 6.73486 19.5599 6.60486 18.8799C6.53486 18.5499 6.35486 18.2899 6.12486 18.0899C6.32486 17.8999 6.45486 17.6499 6.50486 17.3799C6.57486 17.0099 6.49486 16.6299 6.27486 16.3099C5.85486 15.6899 5.07486 15.5199 4.10486 15.8299C3.83486 15.9199 3.69486 16.1999 3.77486 16.4599C3.86486 16.7299 4.14486 16.8699 4.40486 16.7899C4.70486 16.6999 5.24486 16.5799 5.45486 16.8899C5.51486 16.9899 5.54486 17.0999 5.52486 17.1999C5.51486 17.2699 5.47486 17.3599 5.36486 17.4299C5.26486 17.4999 5.12486 17.5399 4.95486 17.5799L4.77486 17.6299C4.54486 17.6999 4.40486 17.9099 4.41486 18.1499C4.42486 18.3899 4.61486 18.5799 4.84486 18.6099C5.20486 18.6599 5.58486 18.8299 5.63486 19.0799C5.67486 19.2999 5.46486 19.5499 5.25486 19.6299C4.94486 19.7599 4.52486 19.7099 4.21486 19.5199C3.97486 19.3699 3.67486 19.4399 3.52486 19.6799C3.37486 19.9199 3.44486 20.2299 3.68486 20.3699ZM20.195 18.7999H9.19497C8.86497 18.7999 8.59497 18.5299 8.59497 18.1999C8.59497 17.8699 8.86497 17.5999 9.19497 17.5999H20.195C20.525 17.5999 20.795 17.8699 20.795 18.1999C20.795 18.5299 20.525 18.7999 20.195 18.7999ZM9.19497 12.5999H20.195C20.525 12.5999 20.795 12.3299 20.795 11.9999C20.795 11.6699 20.525 11.3999 20.195 11.3999H9.19497C8.86497 11.3999 8.59497 11.6699 8.59497 11.9999C8.59497 12.3299 8.86497 12.5999 9.19497 12.5999Z"/></svg>',
},
'|',
{
name: 'clean-block',
action: EasyMDE.cleanBlock,
title: i18n.global.t('input.editor.cleanBlock'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M9.25989 6.18384H20.4513C20.7823 6.18384 21.0509 6.45239 21.0509 6.78345V17.9749C21.0509 18.3059 20.7823 18.5745 20.4513 18.5745H9.25989C9.0929 18.5745 8.93469 18.5061 8.82043 18.384L3.6095 12.7883C3.39563 12.5579 3.39563 12.2004 3.6095 11.97L8.82043 6.37427C8.93469 6.2522 9.0929 6.18384 9.25989 6.18384ZM9.52063 17.3752H19.8517V7.38306H9.52063L4.86926 12.3792L9.52063 17.3752ZM12.7755 15.0686C12.6222 15.0686 12.4679 15.01 12.3517 14.8928C12.1173 14.6584 12.1173 14.2786 12.3517 14.0452L14.0503 12.3469L12.3517 10.6487C12.1173 10.4153 12.1173 10.0354 12.3517 9.80103C12.5841 9.56665 12.965 9.56665 13.1993 9.80103L14.8981 11.4994L16.5968 9.80103C16.8312 9.56665 17.212 9.56665 17.4445 9.80103C17.6788 10.0354 17.6788 10.4153 17.4445 10.6487L15.7458 12.3469L17.4445 14.0452C17.6788 14.2786 17.6788 14.6584 17.4445 14.8928C17.3282 15.01 17.174 15.0686 17.0206 15.0686C16.8673 15.0686 16.714 15.01 16.5968 14.8928L14.8981 13.1945L13.1993 14.8928C13.0822 15.01 12.9288 15.0686 12.7755 15.0686Z"/></svg>',
},
{
name: 'link',
action: EasyMDE.drawLink,
title: i18n.global.t('input.editor.link'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M11.4399 15.3452C11.4999 15.3652 11.5699 15.3752 11.6299 15.3752C11.8799 15.3752 12.1199 15.2152 12.1999 14.9652C12.2999 14.6452 12.1299 14.3052 11.8199 14.2052C11.3499 14.0452 10.9299 13.7852 10.5699 13.4152C10.1999 13.0552 9.9399 12.6452 9.7799 12.1552C9.6599 11.8252 9.5999 11.4652 9.5999 11.0952C9.5999 10.2152 9.9399 9.38518 10.5699 8.75518L15.1599 4.15518C16.4499 2.87518 18.5399 2.87518 19.8299 4.15518C20.4499 4.78518 20.7899 5.61518 20.7899 6.49518C20.7899 7.37518 20.4499 8.20518 19.8299 8.82518L16.7399 11.9052C16.5099 12.1452 16.5099 12.5252 16.7399 12.7552C16.9799 12.9852 17.3599 12.9852 17.5899 12.7552L20.6799 9.67518C21.5299 8.83518 21.9999 7.69518 21.9999 6.49518C21.9999 5.29518 21.5299 4.16518 20.6899 3.30518C18.9299 1.55518 16.0799 1.55518 14.3199 3.30518L9.7299 7.90518C8.8699 8.75518 8.3999 9.88518 8.3999 11.0952C8.3999 11.6152 8.4899 12.1152 8.6499 12.5552C8.8599 13.1952 9.2399 13.7952 9.7199 14.2652C10.1999 14.7552 10.7999 15.1352 11.4399 15.3452ZM3.32 20.6851C4.2 21.5551 5.35 21.9951 6.5 21.9951C7.65 21.9951 8.81 21.5551 9.69 20.7051L14.28 16.1051C15.14 15.2551 15.61 14.1251 15.61 12.9151C15.61 12.4551 15.54 11.9951 15.4 11.5551C15.17 10.8651 14.8 10.2551 14.28 9.73509C13.76 9.21509 13.15 8.84509 12.46 8.61509C12.14 8.51509 11.8 8.68509 11.7 8.99509C11.6 9.30509 11.77 9.64509 12.1 9.75509C12.61 9.91509 13.06 10.1951 13.44 10.5751C13.82 10.9551 14.09 11.4051 14.26 11.9151C14.36 12.2351 14.41 12.5651 14.41 12.9051C14.41 13.7951 14.06 14.6251 13.43 15.2451L8.84 19.8451C7.55 21.1251 5.46 21.1251 4.17 19.8451C3.55 19.2151 3.21 18.3951 3.21 17.5051C3.21 16.6151 3.55 15.7851 4.17 15.1651L7.35 11.9851C7.58 11.7451 7.59 11.3651 7.35 11.1351C7.11 10.9051 6.73 10.9051 6.5 11.1351L3.32 14.3151C2.47 15.1551 2 16.2851 2 17.4951C2 18.7051 2.47 19.8351 3.32 20.6851Z"/></svg>',
},
{
name: 'image',
action: EasyMDE.drawImage,
title: i18n.global.t('input.editor.image'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M4 4C2.89543 4 2 4.89543 2 6V16V17.5152V18C2 19.1046 2.89543 20 4 20H20C21.0528 20 21.9156 19.1866 21.9942 18.1539L22 18.1632V18V16V6C22 4.89543 21.1046 4 20 4H4ZM3.2 17.7V16.5642L6.78192 13.7254C6.8616 13.6622 6.97597 13.6689 7.04776 13.7409L10.3126 17.0146C10.7026 17.4056 11.3357 17.4065 11.7268 17.0165C11.7606 16.9827 11.792 16.9465 11.8207 16.9083L16.736 10.352C16.8023 10.2636 16.9277 10.2457 17.016 10.312C17.0355 10.3265 17.0521 10.3445 17.0651 10.365L20.8 16.2669V17.7C20.8 18.3075 20.3075 18.8 19.7 18.8H4.3C3.69249 18.8 3.2 18.3075 3.2 17.7ZM17.3865 8.61836L20.8 14.08V6.3C20.8 5.69249 20.3075 5.2 19.7 5.2H4.3C3.69249 5.2 3.2 5.69249 3.2 6.3V15.04L6.65054 12.2796C6.84949 12.1204 7.13629 12.1363 7.31645 12.3164L10.8369 15.8369C10.915 15.915 11.0417 15.915 11.1198 15.8369C11.1265 15.8302 16.5625 8.58336 16.5625 8.58336C16.7282 8.36245 17.0416 8.31768 17.2625 8.48336C17.3118 8.52034 17.3538 8.56611 17.3865 8.61836ZM8 8.5C8 9.32843 7.32843 10 6.5 10C5.67157 10 5 9.32843 5 8.5C5 7.67157 5.67157 7 6.5 7C7.32843 7 8 7.67157 8 8.5Z"/></svg>',
},
{
name: 'table',
action: EasyMDE.drawTable,
title: i18n.global.t('input.editor.table'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M6.18524 3.08496H19.4152C20.6752 3.08496 21.7152 4.11496 21.7152 5.38496V18.615C21.7152 19.885 20.6852 20.915 19.4152 20.915H6.18524C4.91524 20.915 3.88525 19.885 3.88525 18.615V5.38496C3.88525 4.11496 4.91524 3.08496 6.18524 3.08496ZM19.4052 19.705C20.0152 19.705 20.5052 19.215 20.5052 18.605H20.5153V5.38496C20.5153 4.77496 20.0252 4.28496 19.4152 4.28496H6.18524C5.57524 4.28496 5.08521 4.77496 5.08521 5.38496V18.605C5.08521 19.215 5.57524 19.705 6.18524 19.705H19.4052ZM17.4453 9.15503H8.15527C7.82527 9.15503 7.5553 9.42503 7.5553 9.75503C7.5553 10.085 7.82527 10.355 8.15527 10.355H17.4453C17.7753 10.355 18.0453 10.085 18.0453 9.75503C18.0453 9.42503 17.7753 9.15503 17.4453 9.15503ZM17.4453 13.635H8.15527C7.82527 13.635 7.5553 13.905 7.5553 14.235C7.5553 14.565 7.82527 14.835 8.15527 14.835H17.4453C17.7753 14.835 18.0453 14.565 18.0453 14.235C18.0453 13.905 17.7753 13.635 17.4453 13.635Z"/></svg>',
},
{
name: 'horizontal-rule',
action: EasyMDE.drawHorizontalRule,
title: i18n.global.t('input.editor.horizontalRule'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M21 13H3C2.45 13 2 12.55 2 12C2 11.45 2.45 11 3 11H21C21.55 11 22 11.45 22 12C22 12.55 21.55 13 21 13Z"/></svg>',
},
'|',
{
name: 'side-by-side',
action: EasyMDE.toggleSideBySide,
title: i18n.global.t('input.editor.sideBySide'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M18.4787 14.58C18.3587 14.69 18.2987 14.85 18.2987 15C18.2987 15.15 18.3587 15.31 18.4787 15.42C18.7187 15.65 19.0987 15.65 19.3287 15.42L22.3287 12.42C22.5587 12.18 22.5587 11.8 22.3287 11.57L19.3287 8.56996C19.0887 8.33996 18.7087 8.33996 18.4787 8.56996C18.2487 8.80996 18.2487 9.18996 18.4787 9.41996L20.451 11.3999L14.4487 11.3999L14.4487 4.6C14.4487 4.27 14.1787 4 13.8487 4C13.5187 4 13.2487 4.27 13.2487 4.6L13.2487 19.4C13.2487 19.73 13.5187 20 13.8487 20C14.1787 20 14.4487 19.73 14.4487 19.4L14.4487 12.5999L20.4511 12.5999L18.4787 14.58ZM9.54878 19.4L9.54878 12.5999L3.5486 12.5999L5.52867 14.58C5.75867 14.81 5.75867 15.19 5.52867 15.43C5.29867 15.66 4.91867 15.66 4.67867 15.43L1.67867 12.43C1.63274 12.384 1.5956 12.3323 1.56725 12.2774C1.53058 12.2077 1.50724 12.1299 1.50068 12.0477C1.49934 12.0317 1.49867 12.0158 1.49867 12C1.49867 11.9841 1.49933 11.9682 1.50067 11.9522C1.51454 11.778 1.60365 11.6242 1.73526 11.5234L4.67867 8.57997C4.90867 8.34997 5.28867 8.34997 5.52867 8.57997C5.75867 8.80997 5.75867 9.18997 5.52867 9.42997L3.55107 11.3999L9.54878 11.3999L9.54878 4.6C9.54878 4.27 9.81878 4 10.1488 4C10.4788 4 10.7488 4.27 10.7488 4.6L10.7488 11.9999L10.7488 19.4C10.7488 19.73 10.4788 20 10.1488 20C9.81878 20 9.54878 19.73 9.54878 19.4Z"/></svg>',
},
{
name: 'guide',
action() {
window.open('https://www.markdownguide.org/basic-syntax/', '_blank')
},
title: i18n.global.t('input.editor.guide'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M19.4999 2.3999H6.4999C5.0699 2.3999 3.8999 3.5699 3.8999 4.9999V18.9999C3.8999 20.4299 5.0699 21.5999 6.4999 21.5999H19.4999C19.8299 21.5999 20.0999 21.3299 20.0999 20.9999V16.9999V2.9999C20.0999 2.6699 19.8299 2.3999 19.4999 2.3999ZM5.0999 4.9999V16.8118C5.50468 16.5513 5.98546 16.3999 6.4999 16.3999H18.8999V3.5999H6.4999C5.7299 3.5999 5.0999 4.2299 5.0999 4.9999ZM6.4999 17.5999H18.8999V20.3999H6.4999C5.7299 20.3999 5.0999 19.7699 5.0999 18.9999C5.0999 18.2299 5.7299 17.5999 6.4999 17.5999ZM8.4999 8.5999H15.4999C15.8299 8.5999 16.0999 8.3299 16.0999 7.9999C16.0999 7.6699 15.8299 7.3999 15.4999 7.3999H8.4999C8.1699 7.3999 7.8999 7.6699 7.8999 7.9999C7.8999 8.3299 8.1699 8.5999 8.4999 8.5999ZM15.4999 11.3999H8.4999C8.1699 11.3999 7.8999 11.6699 7.8999 11.9999C7.8999 12.3299 8.1699 12.5999 8.4999 12.5999H15.4999C15.8299 12.5999 16.0999 12.3299 16.0999 11.9999C16.0999 11.6699 15.8299 11.3999 15.4999 11.3999Z"/></svg>',
},
],
}
}

View File

@ -9,19 +9,24 @@
<div class="control" :class="{'is-loading': loading || localLoading}">
<div
class="input-wrapper input"
:class="{'has-multiple': hasMultiple}"
:class="{'has-multiple': hasMultiple, 'has-removal-button': removalAvailable}"
>
<template v-if="Array.isArray(internalValue)">
<slot
v-if="Array.isArray(internalValue)"
name="items"
:items="internalValue"
:remove="remove"
>
<template v-for="(item, key) in internalValue">
<slot name="tag" :item="item">
<span :key="`item${key}`" class="tag ml-2 mt-2">
{{ label !== '' ? item[label] : item }}
<BaseButton @click="() => remove(item)" class="delete is-small"></BaseButton>
</span>
<span :key="`item${key}`" class="tag ml-2 mt-2">
{{ label !== '' ? item[label] : item }}
<BaseButton @click="() => remove(item)" class="delete is-small"></BaseButton>
</span>
</slot>
</template>
</template>
</slot>
<input
type="text"
class="input"
@ -35,6 +40,13 @@
:autocomplete="autocompleteEnabled ? undefined : 'off'"
:spellcheck="autocompleteEnabled ? undefined : 'false'"
/>
<BaseButton
v-if="removalAvailable"
class="removal-button"
@click="resetSelectedValue"
>
<icon icon="times"/>
</BaseButton>
</div>
</div>
@ -85,7 +97,9 @@
</template>
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance, type PropType} from 'vue'
import {
computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance, type PropType,
} from 'vue'
import {useI18n} from 'vue-i18n'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
@ -93,6 +107,7 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function elementInResults(elem: string | any, label: string, query: string): boolean {
// Don't make create available if we have an exact match in our search results.
if (label !== '') {
@ -121,7 +136,8 @@ const props = defineProps({
* The search results where the @search listener needs to put the results into
*/
searchResults: {
type: Array as PropType<{[id: string]: any}>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as PropType<{ [id: string]: any }>,
default: () => [],
},
/**
@ -136,7 +152,8 @@ const props = defineProps({
* The object with the value, updated every time an entry is selected.
*/
modelValue: {
type: [Object] as PropType<{[key: string]: any}>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: [Object] as PropType<{ [key: string]: any }>,
default: null,
},
/**
@ -152,7 +169,7 @@ const props = defineProps({
createPlaceholder: {
type: String,
default() {
const {t} = useI18n({useScope: 'global'})
const {t} = useI18n({useScope: 'global'})
return t('input.multiselect.createPlaceholder')
},
},
@ -162,7 +179,7 @@ const props = defineProps({
selectPlaceholder: {
type: String,
default() {
const {t} = useI18n({useScope: 'global'})
const {t} = useI18n({useScope: 'global'})
return t('input.multiselect.selectPlaceholder')
},
},
@ -208,30 +225,33 @@ const props = defineProps({
})
const emit = defineEmits<{
(e: 'update:modelValue', value: null): void
(e: 'update:modelValue', value: null): void
/**
* Triggered every time the search query input changes
*/
(e: 'search', query: string): void
(e: 'search', query: string): void
/**
* Triggered every time an option from the search results is selected. Also triggers a change in v-model.
*/
(e: 'select', value: {[key: string]: any}): void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(e: 'select', value: { [key: string]: any }): void
/**
* If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
*/
(e: 'create', query: string): void
(e: 'create', query: string): void
/**
* If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
*/
(e: 'remove', value: null): void
(e: 'remove', value: null): void
}>()
const query = ref<string | {[key: string]: any}>('')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const query = ref<string | { [key: string]: any }>('')
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
const localLoading = ref(false)
const showSearchResults = ref(false)
const internalValue = ref<string | {[key: string]: any} | any[] | null>(null)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const internalValue = ref<string | { [key: string]: any } | any[] | null>(null)
onMounted(() => document.addEventListener('click', hideSearchResultsHandler))
onBeforeUnmount(() => document.removeEventListener('click', hideSearchResultsHandler))
@ -259,17 +279,19 @@ const searchResultsVisible = computed(() => {
})
const creatableAvailable = computed(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hasResult = filteredSearchResults.value.some((elem: any) => elementInResults(elem, props.label, query.value))
const hasQueryAlreadyAdded = Array.isArray(internalValue.value) && internalValue.value.some(elem => elementInResults(elem, props.label, query.value))
return props.creatable
&& query.value !== ''
return props.creatable
&& query.value !== ''
&& !(hasResult || hasQueryAlreadyAdded)
})
const filteredSearchResults = computed(() => {
const currentInternal = internalValue.value
if (props.multiple && currentInternal !== null && Array.isArray(currentInternal)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return searchResults.value.filter((item: any) => !currentInternal.some(e => e === item))
}
@ -280,7 +302,13 @@ const hasMultiple = computed(() => {
return props.multiple && Array.isArray(internalValue.value) && internalValue.value.length > 0
})
const removalAvailable = computed(() => !props.multiple && internalValue.value !== null && query.value !== '')
function resetSelectedValue() {
select(null)
}
const searchInput = ref<HTMLInputElement | null>(null)
// Searching will be triggered with a 200ms delay to avoid searching on every keyup event.
function search() {
@ -305,6 +333,7 @@ function search() {
}
const multiselectRoot = ref<HTMLElement | null>(null)
function hideSearchResultsHandler(e: MouseEvent) {
closeWhenClickedOutside(e, multiselectRoot.value, closeSearchResults)
}
@ -321,12 +350,14 @@ function handleFocus() {
}, 10)
}
function select(object: {[key: string]: any}) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function select(object: { [key: string]: any } | null) {
if (props.multiple) {
if (internalValue.value === null) {
internalValue.value = []
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(internalValue.value as any[]).push(object)
} else {
internalValue.value = object
@ -340,7 +371,8 @@ function select(object: {[key: string]: any}) {
}
}
function setSelectedObject(object: string | {[id: string]: any} | null, resetOnly = false) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setSelectedObject(object: string | { [id: string]: any } | null, resetOnly = false) {
internalValue.value = object
// We assume we're getting an array when multiple is enabled and can therefore leave the query
@ -363,6 +395,7 @@ function setSelectedObject(object: string | {[id: string]: any} | null, resetOnl
}
const results = ref<(Element | ComponentPublicInstance)[]>([])
function setResult(el: Element | ComponentPublicInstance | null, index: number) {
if (el === null) {
delete results.value[index]
@ -408,8 +441,9 @@ function createOrSelectOnEnter() {
if (!creatableAvailable.value) {
// Check if there's an exact match for our search term
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const exactMatch = filteredSearchResults.value.find((elem: any) => elementInResults(elem, props.label, query.value))
if(exactMatch) {
if (exactMatch) {
select(exactMatch)
}
@ -419,6 +453,7 @@ function createOrSelectOnEnter() {
create()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function remove(item: any) {
for (const ind in internalValue.value) {
if (internalValue.value[ind] === item) {
@ -565,4 +600,14 @@ function focus() {
transition: color $transition;
padding-left: .5rem;
}
.has-removal-button {
position: relative;
}
.removal-button {
position: absolute;
right: .5rem;
color: var(--danger);
}
</style>

View File

@ -1,522 +0,0 @@
<template>
<div class="vue-easymde" ref="easymdeRef">
<textarea
class="vue-simplemde-textarea"
:name="name"
:value="modelValue"
@input="handleInput(($event.target as HTMLTextAreaElement).value)"
/>
</div>
</template>
<script setup lang="ts">
import {ref, watch, onMounted, onDeactivated, onBeforeUnmount, nextTick, shallowReactive, type ShallowReactive, type PropType} from 'vue'
import EasyMDE, {toggleFullScreen} from 'easymde'
import {marked} from 'marked'
import type CodeMirror from 'codemirror'
const props = defineProps({
modelValue: {
type: String,
default: '',
},
name: {
type: String,
},
previewClass: {
type: String,
},
autoinit: {
type: Boolean,
default: true,
},
highlight: {
type: Boolean,
default: false,
},
sanitize: {
type: Boolean,
default: false,
},
configs: {
type: Object,
default: () => ({}),
},
previewRender: {
type: Function as PropType<EasyMDE.Options['previewRender']>,
},
})
const emit = defineEmits(['update:modelValue', 'blur', 'initialized'])
const isValueUpdateFromInner = ref(false)
let easymde: ShallowReactive<EasyMDE> | undefined
onMounted(() => {
if (props.autoinit) initialize()
})
onDeactivated(() => {
if (easymde === undefined) return
if (easymde.isFullscreenActive()) toggleFullScreen(easymde)
easymde.toTextArea
})
onBeforeUnmount(() => {
if (easymde) {
easymde.toTextArea()
easymde.cleanup()
easymde = undefined
}
})
const easymdeRef = ref<HTMLElement | null>(null)
function initialize() {
const configs: EasyMDE.Options = Object.assign({
element: easymdeRef.value?.firstElementChild as HTMLElement,
initialValue: props.modelValue,
previewRender: props.previewRender,
renderingConfig: {},
}, props.configs)
// Synchronize the values of value and initialValue
if (configs.initialValue) {
emit('update:modelValue', configs.initialValue)
}
// Determine whether to enable code highlighting
if (props.highlight) {
configs.renderingConfig!.codeSyntaxHighlighting = true
}
// Set whether to render the input html
marked.setOptions({ sanitize: props.sanitize })
// Instantiated editor
easymde = shallowReactive(new EasyMDE(configs))
// Add a custom previewClass
const className = props.previewClass || ''
addPreviewClass(easymde, className)
// Binding event
easymde.codemirror.on('change', handleCodemirrorInput)
easymde.codemirror.on('blur', handleCodemirrorBlur)
nextTick(() => emit('initialized', easymde))
}
function addPreviewClass(easymde: EasyMDE, className: string) {
const wrapper = easymde.codemirror.getWrapperElement()
const preview = document.createElement('div')
wrapper.nextSibling.className += ` ${className}`
preview.className = `editor-preview ${className}`
wrapper.appendChild(preview)
}
function handleInput(val: string) {
isValueUpdateFromInner.value = true
emit('update:modelValue', val)
}
function handleCodemirrorInput(instance: CodeMirror.Editor, changeObj: CodeMirror.EditorChange) {
if (changeObj.origin === 'setValue' || easymde === undefined) {
return
}
handleInput(easymde.value())
}
function handleCodemirrorBlur() {
if (easymde === undefined) {
return
}
isValueUpdateFromInner.value = true
emit('blur', easymde.value())
}
watch(
() => props.modelValue,
(val) => {
if (isValueUpdateFromInner.value) {
isValueUpdateFromInner.value = false
} else {
easymde?.value(val)
}
},
)
</script>
<style lang="scss">
.EasyMDEContainer {
display: block;
}
.EasyMDEContainer.sided--no-fullscreen {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.EasyMDEContainer .CodeMirror {
box-sizing: border-box;
height: auto;
border: 1px solid #ddd;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 10px;
font: inherit;
z-index: 0;
word-wrap: break-word;
}
.EasyMDEContainer .CodeMirror-scroll {
cursor: text;
}
.EasyMDEContainer .CodeMirror-fullscreen {
background: #fff;
position: fixed !important;
top: 50px;
left: 0;
right: 0;
bottom: 0;
height: auto;
z-index: 8;
border-right: none !important;
border-bottom-right-radius: 0 !important;
}
.EasyMDEContainer .CodeMirror-sided {
width: 50% !important;
}
.EasyMDEContainer.sided--no-fullscreen .CodeMirror-sided {
border-right: none!important;
border-bottom-right-radius: 0px;
position: relative;
flex: 1 1 auto;
}
.EasyMDEContainer .CodeMirror-placeholder {
opacity: .5;
}
.EasyMDEContainer .CodeMirror-focused .CodeMirror-selected {
background: #d9d9d9;
}
.editor-toolbar {
position: relative;
user-select: none;
padding: 9px 10px;
border-top: 1px solid #bbb;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.editor-toolbar.fullscreen {
width: 100%;
height: 50px;
padding-top: 10px;
padding-bottom: 10px;
box-sizing: border-box;
background: #fff;
border: 0;
position: fixed;
top: 0;
left: 0;
opacity: 1;
z-index: 9;
}
.editor-toolbar.fullscreen::before {
width: 20px;
height: 50px;
background: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 0;
}
.editor-toolbar.fullscreen::after {
width: 20px;
height: 50px;
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
position: fixed;
top: 0;
right: 0;
margin: 0;
padding: 0;
}
.EasyMDEContainer.sided--no-fullscreen .editor-toolbar {
width: 100%;
}
.editor-toolbar button, .editor-toolbar .easymde-dropdown {
background: transparent;
display: inline-block;
text-align: center;
text-decoration: none !important;
height: 30px;
margin: 0;
padding: 0;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
}
.editor-toolbar button {
width: 30px;
}
.editor-toolbar button.active,
.editor-toolbar button:hover {
background: #fcfcfc;
border-color: #95a5a6;
}
.editor-toolbar i.separator {
display: inline-block;
width: 0;
border-left: 1px solid #d9d9d9;
border-right: 1px solid #fff;
color: transparent;
text-indent: -10px;
margin: 0 6px;
}
.editor-toolbar button:after {
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size: 65%;
vertical-align: text-bottom;
position: relative;
top: 2px;
}
.editor-toolbar button.heading-1:after {
content: "1";
}
.editor-toolbar button.heading-2:after {
content: "2";
}
.editor-toolbar button.heading-3:after {
content: "3";
}
.editor-toolbar button.heading-bigger:after {
content: "▲";
}
.editor-toolbar button.heading-smaller:after {
content: "▼";
}
.editor-toolbar.disabled-for-preview button:not(.no-disable) {
opacity: .6;
pointer-events: none;
}
@media only screen and (max-width: 700px) {
.editor-toolbar i.no-mobile {
display: none;
}
}
.editor-statusbar {
padding: 8px 10px;
font-size: 12px;
color: #959694;
text-align: right;
}
.EasyMDEContainer.sided--no-fullscreen .editor-statusbar {
width: 100%;
}
.editor-statusbar span {
display: inline-block;
min-width: 4em;
margin-left: 1em;
}
.editor-statusbar .lines:before {
content: 'lines: '
}
.editor-statusbar .words:before {
content: 'words: '
}
.editor-statusbar .characters:before {
content: 'characters: '
}
.editor-preview-full {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 7;
overflow: auto;
display: none;
box-sizing: border-box;
}
.editor-preview-side {
position: fixed;
bottom: 0;
width: 50%;
top: 50px;
right: 0;
z-index: 9;
overflow: auto;
display: none;
box-sizing: border-box;
border: 1px solid #ddd;
word-wrap: break-word;
}
.editor-preview-active-side {
display: block
}
.EasyMDEContainer.sided--no-fullscreen .editor-preview-active-side {
flex: 1 1 auto;
height: auto;
position: static;
}
.editor-preview-active {
display: block
}
.editor-preview {
padding: 10px;
background: #fafafa;
}
.editor-preview > p {
margin-top: 0
}
.editor-preview pre {
background: #eee;
margin-bottom: 10px;
}
.editor-preview table td,
.editor-preview table th {
border: 1px solid #ddd;
padding: 5px;
}
.cm-s-easymde .cm-tag {
color: #63a35c;
}
.cm-s-easymde .cm-attribute {
color: #795da3;
}
.cm-s-easymde .cm-string {
color: #183691;
}
.cm-s-easymde .cm-header-1 {
font-size: 200%;
line-height: 200%;
}
.cm-s-easymde .cm-header-2 {
font-size: 160%;
line-height: 160%;
}
.cm-s-easymde .cm-header-3 {
font-size: 125%;
line-height: 125%;
}
.cm-s-easymde .cm-header-4 {
font-size: 110%;
line-height: 110%;
}
.cm-s-easymde .cm-comment {
background: rgba(0, 0, 0, .05);
border-radius: 2px;
}
.cm-s-easymde .cm-link {
color: #7f8c8d;
}
.cm-s-easymde .cm-url {
color: #aab2b3;
}
.cm-s-easymde .cm-quote {
color: #7f8c8d;
font-style: italic;
}
.editor-toolbar .easymde-dropdown {
position: relative;
background: linear-gradient(to bottom right, #fff 0%, #fff 84%, #333 50%, #333 100%);
border-radius: 0;
border: 1px solid #fff;
}
.editor-toolbar .easymde-dropdown:hover {
background: linear-gradient(to bottom right, #fff 0%, #fff 84%, #333 50%, #333 100%);
}
.easymde-dropdown-content {
display: block;
visibility: hidden;
position: absolute;
background-color: #f9f9f9;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
padding: 8px;
z-index: 2;
top: 30px;
}
.easymde-dropdown:active .easymde-dropdown-content,
.easymde-dropdown:focus .easymde-dropdown-content {
visibility: visible;
}
span[data-img-src]::after{
content: '';
background-image: var(--bg-image);
display: block;
max-height: 100%;
max-width: 100%;
background-size: contain;
height: 0;
padding-top: var(--height);
width: var(--width);
background-repeat: no-repeat;
}
</style>
<style lang="scss" scoped>
.vue-easymde .markdown-body {
padding: 0.5em
}
.vue-easymde .editor-preview-active,
.vue-easymde .editor-preview-active-side {
display: block;
}
</style>

View File

@ -1,6 +1,7 @@
<template>
<transition :name="name">
<slot />
<!-- eslint-disable-next-line -->
<slot/>
</transition>
</template>

View File

@ -2,11 +2,17 @@ import {library} from '@fortawesome/fontawesome-svg-core'
import {
faAlignLeft,
faAngleRight,
faAnglesUp,
faArchive,
faArrowLeft,
faArrowUpFromBracket,
faBold,
faItalic,
faStrikethrough,
faCode,
faBars,
faBell,
faBolt,
faCalendar,
faCheck,
faCheckDouble,
@ -27,6 +33,7 @@ import {
faFilter,
faForward,
faGripLines,
faHeader,
faHistory,
faImage,
faKeyboard,
@ -57,14 +64,26 @@ import {
faTimes,
faTrashAlt,
faUser,
faUsers, faX,
faUsers,
faQuoteRight,
faListUl,
faLink,
faUndo,
faRedo,
faUnlink,
faParagraph,
faTable,
faX, faArrowTurnDown, faListCheck, faXmark, faXmarksLines, faFont, faRulerHorizontal, faUnderline,
} from '@fortawesome/free-solid-svg-icons'
import {
faBellSlash,
faCalendarAlt,
faCheckSquare,
faClock,
faComments,
faFileImage,
faSave,
faSquareCheck,
faStar,
faSun,
faTimesCircle,
@ -74,6 +93,21 @@ import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import type {FontAwesomeIcon as FontAwesomeIconFixedTypes} from '@/types/vue-fontawesome'
library.add(faBold)
library.add(faUndo)
library.add(faRedo)
library.add(faItalic)
library.add(faLink)
library.add(faUnlink)
library.add(faParagraph)
library.add(faSquareCheck)
library.add(faTable)
library.add(faFileImage)
library.add(faCheckSquare)
library.add(faStrikethrough)
library.add(faCode)
library.add(faQuoteRight)
library.add(faListUl)
library.add(faAlignLeft)
library.add(faAngleRight)
library.add(faArchive)
@ -105,6 +139,7 @@ library.add(faFillDrip)
library.add(faFilter)
library.add(faForward)
library.add(faGripLines)
library.add(faHeader)
library.add(faHistory)
library.add(faImage)
library.add(faKeyboard)
@ -142,6 +177,15 @@ library.add(faUser)
library.add(faUsers)
library.add(faArrowUpFromBracket)
library.add(faX)
library.add(faAnglesUp)
library.add(faBolt)
library.add(faArrowTurnDown)
library.add(faListCheck)
library.add(faXmark)
library.add(faXmarksLines)
library.add(faFont)
library.add(faRulerHorizontal)
library.add(faUnderline)
// overwriting the wrong types
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import BaseButton from '@/components/base/BaseButton.vue'
import {useBaseStore} from '@/stores/base'
import {onBeforeUnmount, onMounted} from 'vue'
import {eventToHotkeyString} from '@github/hotkey'
const baseStore = useBaseStore()
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
function openQuickActionsViaHotkey(event) {
const hotkeyString = eventToHotkeyString(event)
if (!hotkeyString) return
if (hotkeyString !== 'Control+k' && hotkeyString !== 'Meta+k') return
event.preventDefault()
openQuickActions()
}
onMounted(() => {
document.addEventListener('keydown', openQuickActionsViaHotkey)
})
onBeforeUnmount(() => {
document.removeEventListener('keydown', openQuickActionsViaHotkey)
})
function openQuickActions() {
baseStore.setQuickActionsActive(true)
}
</script>
<template>
<BaseButton
@click="openQuickActions"
class="trigger-button"
:title="$t('keyboardShortcuts.quickSearch')"
>
<icon icon="search"/>
</BaseButton>
</template>

View File

@ -1,21 +1,26 @@
<template>
<BaseButton class="dropdown-item">
<span class="icon" v-if="icon">
<span
v-if="icon"
class="icon is-small"
:class="iconClass"
>
<Icon :icon="icon"/>
</span>
<span>
<slot />
<slot/>
</span>
</BaseButton>
</template>
<script lang="ts" setup>
import BaseButton, { type BaseButtonProps } from '@/components/base//BaseButton.vue'
import BaseButton, {type BaseButtonProps} from '@/components/base//BaseButton.vue'
import Icon from '@/components/misc/Icon'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
export interface DropDownItemProps extends /* @vue-ignore */ BaseButtonProps {
icon?: IconProp,
iconClass?: object | string,
}
defineProps<DropDownItemProps>()
@ -24,7 +29,6 @@ defineProps<DropDownItemProps>()
<style scoped lang="scss">
.dropdown-item {
color: var(--text);
display: block;
font-size: 0.875rem;
line-height: 1.5;
padding: $item-padding;
@ -52,10 +56,7 @@ defineProps<DropDownItemProps>()
.icon {
padding-right: .5rem;
&:not(.has-text-success) {
color: var(--grey-300) !important;
}
color: var(--grey-300);
}
.has-text-danger .icon {

View File

@ -1,10 +1,10 @@
<template>
<input
type="text"
data-input
:disabled="disabled"
data-input
:disabled="disabled"
v-bind="attrs"
ref="root"
ref="root"
/>
</template>
@ -20,39 +20,39 @@ type Options = flatpickr.Options.Options
type DateOption = flatpickr.Options.DateOption
function camelToKebab(string: string) {
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}
function arrayify<T = unknown>(obj: T) {
return obj instanceof Array
return obj instanceof Array
? obj
: [obj]
}
function nullify<T = unknown>(value: T) {
return (value && (value as unknown[]).length)
return (value && (value as unknown[]).length)
? value
: null
}
// Events to emit, copied from flatpickr source
const includedEvents = [
'onChange',
'onClose',
'onDestroy',
'onMonthChange',
'onOpen',
'onYearChange',
'onChange',
'onClose',
'onDestroy',
'onMonthChange',
'onOpen',
'onYearChange',
] as HookKey[]
// Let's not emit these events by default
const excludedEvents = [
'onValueUpdate',
'onDayCreate',
'onParseConfig',
'onReady',
'onPreCalendarPosition',
'onKeyDown',
'onValueUpdate',
'onDayCreate',
'onParseConfig',
'onReady',
'onPreCalendarPosition',
'onKeyDown',
] as HookKey[]
// Keep a copy of all events for later use
@ -100,19 +100,19 @@ const attrs = useAttrs()
const root = ref<HTMLInputElement | null>(null)
const fp = ref<flatpickr.Instance | null>(null)
const safeConfig = ref<Options>({ ...props.config })
const safeConfig = ref<Options>({...props.config})
function prepareConfig() {
// Don't mutate original object on parent component
const newConfig: Options = { ...props.config }
const newConfig: Options = {...props.config}
props.events.forEach((hook) => {
// Respect global callbacks registered via setDefault() method
const globalCallbacks = flatpickr.defaultConfig[hook] || []
// Inject our own method along with user callback
const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
// Overwrite with merged array
newConfig[hook] = arrayify(newConfig[hook] || []).concat(
globalCallbacks,
@ -147,9 +147,9 @@ onMounted(() => {
prepareConfig()
/**
* Get the HTML node where flatpickr to be attached
* Bind on parent element if wrap is true
*/
* Get the HTML node where flatpickr to be attached
* Bind on parent element if wrap is true
*/
const element = props.config.wrap
? root.value.parentNode
: root.value
@ -179,7 +179,7 @@ watch(config, () => {
fp.value.set(name, safeConfig.value[name])
}
})
}, {deep:true})
}, {deep: true})
const fpInput = computed(() => {
if (!fp.value) return
@ -198,8 +198,8 @@ watchEffect(() => fpInput.value?.addEventListener('blur', onBlur))
onBeforeUnmount(() => fpInput.value?.removeEventListener('blur', onBlur))
/**
* Watch for the disabled property and sets the value to the real input.
*/
* Watch for the disabled property and sets the value to the real input.
*/
watchEffect(() => {
if (disabled.value) {
fpInput.value?.setAttribute('disabled', '')

View File

@ -152,6 +152,10 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
title: 'keyboardShortcuts.task.favorite',
keys: ['s'],
},
{
title: 'keyboardShortcuts.task.save',
keys: [ctrl, 's'],
},
],
},
]

View File

@ -14,7 +14,7 @@
>
<div
class="modal-container"
@click.self.prevent.stop="$emit('close')"
@mousedown.self.prevent.stop="$emit('close')"
v-shortcut="'Escape'"
>
<div
@ -195,10 +195,9 @@ $modal-width: 1024px;
}
.close {
$close-button-min-space: 84px;
$close-button-padding: 26px;
position: fixed;
top: 5px;
top: .5rem;
right: $close-button-padding;
color: var(--grey-900);
font-size: 2rem;
@ -213,6 +212,10 @@ $modal-width: 1024px;
@media screen and (min-width: calc(#{$desktop } + #{$close-button-min-space})) {
color: var(--white);
}
@media screen and (min-width: $tablet) and (max-width: #{$desktop + $close-button-min-space}) {
top: .75rem;
}
}
</style>

View File

@ -48,13 +48,14 @@ import Message from '@/components/misc/message.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
import {ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
import {useOnline} from '@/composables/useOnline'
import {getAuthForRoute} from '@/router'
import {useBaseStore} from '@/stores/base'
import {useAuthStore} from '@/stores/auth'
import {useI18n} from 'vue-i18n'
const router = useRouter()
const route = useRoute()
@ -68,6 +69,8 @@ const online = useOnline()
const error = ref('')
const showLoading = computed(() => !ready.value && error.value === '')
const {t} = useI18n()
async function load() {
try {
await baseStore.loadApp()
@ -77,7 +80,15 @@ async function load() {
await router.push(redirectTo)
}
} catch (e: unknown) {
error.value = String(e)
if (e instanceof NoApiUrlProvidedError) {
error.value = ERROR_NO_API_URL
return
}
if (e instanceof InvalidApiUrlProvidedError) {
error.value = t('apiConfig.error')
return
}
error.value = String(e.message)
}
}

View File

@ -36,6 +36,14 @@
</span>
</div>
</div>
<x-button
v-if="notifications.length > 0 && unreadNotifications > 0"
@click="markAllRead"
variant="tertiary"
class="mt-2 is-fullwidth"
>
{{ $t('notification.markAllRead') }}
</x-button>
<p class="nothing" v-if="notifications.length === 0">
{{ $t('notification.none') }}<br/>
<span class="explainer">
@ -60,11 +68,15 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import {getDisplayName} from '@/models/user'
import {useAuthStore} from '@/stores/auth'
import XButton from '@/components/input/button.vue'
import {success} from '@/message'
import {useI18n} from 'vue-i18n'
const LOAD_NOTIFICATIONS_INTERVAL = 10000
const authStore = useAuthStore()
const router = useRouter()
const {t} = useI18n()
const allNotifications = ref<INotification[]>([])
const showNotifications = ref(false)
@ -138,6 +150,12 @@ function to(n, index) {
allNotifications.value[index] = await notificationService.update(n)
}
}
async function markAllRead() {
const notificationService = new NotificationService()
await notificationService.markAllRead()
success({message: t('notification.markAllReadSuccess')})
}
</script>
<style lang="scss" scoped>

View File

@ -33,7 +33,7 @@
}"
/>
<BaseButton
v-if="!project.isArchived"
v-if="!project.isArchived && project.id > -1"
class="favorite"
:class="{'is-favorite': project.isFavorite}"
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"

View File

@ -155,14 +155,16 @@
</div>
<template
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)">
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)"
>
<div class="field">
<label class="label">{{ $t('project.lists') }}</label>
<label class="label">{{ $t('project.projects') }}</label>
<div class="control">
<SelectProject
v-model="entities.projects"
@select="changeMultiselectFilter('projects', 'project_id')"
@remove="changeMultiselectFilter('projects', 'project_id')"
:project-filter="p => p.id > 0"
/>
</div>
</div>
@ -311,7 +313,7 @@ function prepareFilters() {
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
prepareDate('reminders')
prepareRelatedObjectFilter('users', 'assignees')
prepareRelatedObjectFilter('projects', 'project_id')
prepareProjectsFilter()
prepareSingleValue('labels')
@ -410,10 +412,10 @@ function prepareDate(filterName, variableName) {
const endDate = new Date(params.value.filter_value[foundDateEnd])
filters.value[variableName] = {
dateFrom: !isNaN(startDate)
? `${startDate.getFullYear()}-${startDate.getMonth() + 1}-${startDate.getDate()}`
? `${startDate.getUTCFullYear()}-${startDate.getUTCMonth() + 1}-${startDate.getUTCDate()}`
: params.value.filter_value[foundDateStart],
dateTo: !isNaN(endDate)
? `${endDate.getFullYear()}-${endDate.getMonth() + 1}-${endDate.getDate()}`
? `${endDate.getUTCFullYear()}-${endDate.getUTCMonth() + 1}-${endDate.getUTCDate()}`
: params.value.filter_value[foundDateEnd],
}
}
@ -514,6 +516,11 @@ async function prepareRelatedObjectFilter(kind: EntityType, filterName = null, s
entities[kind] = await services[servicePrefix].getAll({}, {s: filters.value[filterName]})
}
async function prepareProjectsFilter() {
await prepareRelatedObjectFilter('projects', 'project_id')
entities.projects = entities.projects.filter(p => p.id > 0)
}
function setDoneFilter() {
if (filters.value.done) {
removePropertyFromFilter('done')

View File

@ -72,6 +72,12 @@
@update:model-value="setSubscriptionInStore"
type="dropdown"
/>
<dropdown-item
:to="{ name: 'project.settings.webhooks', params: { projectId: project.id } }"
icon="bolt"
>
{{ $t('project.webhooks.title') }}
</dropdown-item>
<dropdown-item
v-if="level < 2"
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"

View File

@ -24,7 +24,7 @@
{{ hintText }}
</div>
<quick-add-magic class="p-2 modal-container-smaller" v-if="isNewTaskCommand"/>
<quick-add-magic v-if="isNewTaskCommand"/>
<div class="results" v-if="selectedCmd === null">
<div v-for="(r, k) in results" :key="k" class="result">
@ -44,7 +44,18 @@
@keyup.prevent.enter="doAction(r.type, i)"
@keyup.prevent.esc="searchInput?.focus()"
>
{{ i.title }}
<template v-if="r.type === ACTION_TYPE.LABELS">
<x-label :label="i"/>
</template>
<template v-else-if="r.type === ACTION_TYPE.TASK">
<single-task-inline-readonly
:task="i"
:show-project="true"
/>
</template>
<template v-else>
{{ i.title }}
</template>
</BaseButton>
</div>
</div>
@ -66,6 +77,8 @@ import ProjectModel from '@/models/project'
import BaseButton from '@/components/base/BaseButton.vue'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import XLabel from '@/components/tasks/partials/label.vue'
import SingleTaskInlineReadonly from '@/components/tasks/partials/singleTaskInlineReadonly.vue'
import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects'
@ -80,6 +93,7 @@ import {success} from '@/message'
import type {ITeam} from '@/modelTypes/ITeam'
import type {ITask} from '@/modelTypes/ITask'
import type {IProject} from '@/modelTypes/IProject'
import type {IAbstract} from '@/modelTypes/IAbstract'
const {t} = useI18n({useScope: 'global'})
const router = useRouter()
@ -90,13 +104,14 @@ const labelStore = useLabelStore()
const taskStore = useTaskStore()
const authStore = useAuthStore()
type DoAction<Type = any> = { type: ACTION_TYPE } & Type
type DoAction<Type> = { type: ACTION_TYPE } & Type
enum ACTION_TYPE {
CMD = 'cmd',
TASK = 'task',
PROJECT = 'project',
TEAM = 'team',
LABELS = 'labels',
}
enum COMMAND_TYPE {
@ -134,24 +149,38 @@ function closeQuickActions() {
}
const foundProjects = computed(() => {
const { project } = parsedQuery.value
if (
searchMode.value === SEARCH_MODE.ALL ||
searchMode.value === SEARCH_MODE.PROJECTS ||
project === null
) {
const {project, text, labels, assignees} = parsedQuery.value
if (project !== null) {
return projectStore.searchProject(project ?? text)
.filter(p => Boolean(p))
}
if (labels.length > 0 || assignees.length > 0) {
return []
}
const history = getHistory()
const allProjects = [
...new Set([
...history.map((l) => projectStore.projects[l.id]),
...projectStore.searchProject(project),
]),
]
if (text === '') {
const history = getHistory()
return history.map((p) => projectStore.projects[p.id])
.filter(p => Boolean(p))
}
return allProjects.filter(l => Boolean(l))
return projectStore.searchProject(project ?? text)
.filter(p => Boolean(p))
})
const foundLabels = computed(() => {
const {labels, text} = parsedQuery.value
if (text === '' && labels.length === 0) {
return []
}
if (labels.length > 0) {
return labelStore.filterLabelsByQuery([], labels[0])
}
return labelStore.filterLabelsByQuery([], text)
})
// FIXME: use fuzzysearch
@ -162,7 +191,7 @@ const foundCommands = computed(() => availableCmds.value.filter((a) =>
interface Result {
type: ACTION_TYPE
title: string
items: DoAction<any>
items: DoAction<IAbstract>
}
const results = computed<Result[]>(() => {
@ -172,15 +201,20 @@ const results = computed<Result[]>(() => {
title: t('quickActions.commands'),
items: foundCommands.value,
},
{
type: ACTION_TYPE.PROJECT,
title: t('quickActions.projects'),
items: foundProjects.value,
},
{
type: ACTION_TYPE.TASK,
title: t('quickActions.tasks'),
items: foundTasks.value,
},
{
type: ACTION_TYPE.PROJECT,
title: t('quickActions.projects'),
items: foundProjects.value,
type: ACTION_TYPE.LABELS,
title: t('quickActions.labels'),
items: foundLabels.value,
},
{
type: ACTION_TYPE.TEAM,
@ -190,7 +224,7 @@ const results = computed<Result[]>(() => {
].filter((i) => i.items.length > 0)
})
const loading = computed(() =>
const loading = computed(() =>
taskService.loading ||
projectStore.isLoading ||
teamService.loading,
@ -262,10 +296,12 @@ const searchMode = computed(() => {
if (query.value === '') {
return SEARCH_MODE.ALL
}
const { text, project, labels, assignees } = parsedQuery.value
const {text, project, labels, assignees} = parsedQuery.value
if (assignees.length === 0 && text !== '') {
return SEARCH_MODE.TASKS
}
if (
assignees.length === 0 &&
project !== null &&
@ -274,6 +310,7 @@ const searchMode = computed(() => {
) {
return SEARCH_MODE.PROJECTS
}
if (
assignees.length > 0 &&
project === null &&
@ -282,6 +319,7 @@ const searchMode = computed(() => {
) {
return SEARCH_MODE.TEAMS
}
return SEARCH_MODE.ALL
})
@ -292,12 +330,12 @@ const isNewTaskCommand = computed(() => (
const taskSearchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
type Filter = {by: string, value: string | number, comparator: string}
type Filter = { by: string, value: string | number, comparator: string }
function filtersToParams(filters: Filter[]) {
const filter_by : Filter['by'][] = []
const filter_value : Filter['value'][] = []
const filter_comparator : Filter['comparator'][] = []
const filter_by: Filter['by'][] = []
const filter_value: Filter['value'][] = []
const filter_comparator: Filter['comparator'][] = []
filters.forEach(({by, value, comparator}) => {
filter_by.push(by)
@ -315,7 +353,8 @@ function filtersToParams(filters: Filter[]) {
function searchTasks() {
if (
searchMode.value !== SEARCH_MODE.ALL &&
searchMode.value !== SEARCH_MODE.TASKS
searchMode.value !== SEARCH_MODE.TASKS &&
searchMode.value !== SEARCH_MODE.PROJECTS
) {
foundTasks.value = []
return
@ -330,7 +369,7 @@ function searchTasks() {
taskSearchTimeout.value = null
}
const { text, project: projectName, labels } = parsedQuery.value
const {text, project: projectName, labels} = parsedQuery.value
const filters: Filter[] = []
@ -349,8 +388,9 @@ function searchTasks() {
if (projectName !== null) {
const project = projectStore.findProjectByExactname(projectName)
console.log({project})
if (project !== null) {
addFilter('projectId', project.id, 'equals')
addFilter('project_id', project.id, 'equals')
}
}
@ -361,19 +401,16 @@ function searchTasks() {
}
}
const params = {
s: text,
...filtersToParams(filters),
}
const params = {
s: text,
sort_by: 'done',
...filtersToParams(filters),
}
taskSearchTimeout.value = setTimeout(async () => {
const r = await taskService.getAll({}, params) as DoAction<ITask>[]
const r = await taskService.getAll({}, params) as DoAction<ITask>[]
foundTasks.value = r.map((t) => {
t.type = ACTION_TYPE.TASK
const project = projectStore.projects[t.projectId]
if (project !== null) {
t.title = `${t.title} (${project.title})`
}
return t
})
}, 150)
@ -396,10 +433,10 @@ function searchTeams() {
clearTimeout(teamSearchTimeout.value)
teamSearchTimeout.value = null
}
const { assignees } = parsedQuery.value
const {assignees} = parsedQuery.value
teamSearchTimeout.value = setTimeout(async () => {
const teamSearchPromises = assignees.map((t) =>
teamService.getAll({}, { s: t }),
teamService.getAll({}, {s: t}),
)
const teamsResult = await Promise.all(teamSearchPromises)
foundTeams.value = teamsResult.flat().map((team) => {
@ -422,21 +459,21 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
closeQuickActions()
await router.push({
name: 'project.index',
params: { projectId: (item as DoAction<IProject>).id },
params: {projectId: (item as DoAction<IProject>).id},
})
break
case ACTION_TYPE.TASK:
closeQuickActions()
await router.push({
name: 'task.detail',
params: { id: (item as DoAction<ITask>).id },
params: {id: (item as DoAction<ITask>).id},
})
break
case ACTION_TYPE.TEAM:
closeQuickActions()
await router.push({
name: 'teams.edit',
params: { id: (item as DoAction<ITeam>).id },
params: {id: (item as DoAction<ITeam>).id},
})
break
case ACTION_TYPE.CMD:
@ -444,6 +481,11 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
selectedCmd.value = item as DoAction<Command>
searchInput.value?.focus()
break
case ACTION_TYPE.LABELS:
query.value = '*' + item.title
searchInput.value?.focus()
searchTasks()
break
}
}
@ -470,8 +512,8 @@ async function newTask() {
title: query.value,
projectId: currentProject.value.id,
})
success({ message: t('task.createSuccess') })
await router.push({ name: 'task.detail', params: { id: task.id } })
success({message: t('task.createSuccess')})
await router.push({name: 'task.detail', params: {id: task.id}})
}
async function newProject() {
@ -481,17 +523,17 @@ async function newProject() {
await projectStore.createProject(new ProjectModel({
title: query.value,
}))
success({ message: t('project.create.createdSuccess')})
success({message: t('project.create.createdSuccess')})
}
async function newTeam() {
const newTeam = new TeamModel({ name: query.value })
const newTeam = new TeamModel({name: query.value})
const team = await teamService.create(newTeam)
await router.push({
name: 'teams.edit',
params: { id: team.id },
params: {id: team.id},
})
success({ message: t('team.create.success') })
success({message: t('team.create.success')})
}
type BaseButtonInstance = InstanceType<typeof BaseButton>
@ -502,7 +544,7 @@ function setResultRefs(el: Element | ComponentPublicInstance | null, index: numb
resultRefs.value[index] = []
}
resultRefs.value[index][key] = el as (BaseButtonInstance | null)
resultRefs.value[index][key] = el as (BaseButtonInstance | null)
}
function select(parentIndex: number, index: number) {
@ -547,7 +589,7 @@ function reset() {
<style lang="scss" scoped>
.quick-actions {
overflow: hidden;
// FIXME: changed position should be an option of the modal
:deep(.modal-content) {
top: 3rem;
@ -569,6 +611,7 @@ function reset() {
}
}
.active-cmd {
font-size: 1.25rem;
margin-left: .5rem;
@ -614,10 +657,4 @@ function reset() {
background: var(--grey-100);
}
}
// HACK:
// FIXME:
.modal-container-smaller :deep(.hint-modal .modal-container) {
height: calc(100vh - 5rem);
}
</style>

View File

@ -3,7 +3,7 @@
v-if="props.isLoading && !ganttBars.length || dayjsLanguageLoading"
class="gantt-container"
/>
<div class="gantt-container" v-else>
<div ref="ganttContainer" class="gantt-container" v-else>
<GGanttChart
:date-format="DAYJS_ISO_DATE_FORMAT"
:chart-start="isoToKebabDate(filters.dateFrom)"
@ -14,7 +14,7 @@
:grid="true"
@dragend-bar="updateGanttTask"
@dblclick-bar="openTask"
:width="ganttChartWidth + 'px'"
:width="ganttChartWidth"
>
<template #timeunit="{value, date}">
<div
@ -85,6 +85,8 @@ const dayjsLanguageLoading = ref(false)
// const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
extendDayjs()
const ganttContainer = ref(null)
const router = useRouter()
const dateFromDate = computed(() => new Date(new Date(filters.value.dateFrom).setHours(0,0,0,0)))
@ -92,9 +94,15 @@ const dateToDate = computed(() => new Date(new Date(filters.value.dateTo).setHou
const DAY_WIDTH_PIXELS = 30
const ganttChartWidth = computed(() => {
const dateDiff = Math.floor((dateToDate.value.valueOf() - dateFromDate.value.valueOf()) / MILLISECONDS_A_DAY)
return dateDiff * DAY_WIDTH_PIXELS
const ganttContainerReference = ganttContainer?.value
const ganttContainerWidth = ganttContainerReference ? (ganttContainerReference['clientWidth'] ?? 0) : 0
const dateDiff = Math.floor((dateToDate.value.valueOf() - dateFromDate.value.valueOf()) / MILLISECONDS_A_DAY)
const calculatedWidth = dateDiff * DAY_WIDTH_PIXELS
return (calculatedWidth > ganttContainerWidth) ? calculatedWidth + 'px' : '100%'
})
const ganttBars = ref<GanttBarObject[][]>([])

View File

@ -178,7 +178,7 @@ async function addTask() {
return rel
})
await Promise.all(relations)
} catch (e: any) {
} catch (e) {
newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_PROJECT') {
errorMessage.value = t('project.create.addProjectRequired')

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import type {IUser} from '@/modelTypes/IUser'
import BaseButton from '@/components/base/BaseButton.vue'
import User from '@/components/misc/user.vue'
import {computed} from 'vue'
const {
assignees,
remove,
disabled,
avatarSize = 30,
inline = false,
} = defineProps<{
assignees: IUser[],
remove?: (user: IUser) => void,
disabled?: boolean,
avatarSize?: number,
inline?: boolean,
}>()
const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
</script>
<template>
<div class="assignees-list" :class="{'is-inline': inline}">
<span
v-for="user in assignees"
class="assignee"
:key="user.id"
>
<User
:key="'user'+user.id"
:avatar-size="avatarSize"
:show-username="false"
:user="user"
:class="{'m-2': hasDelete, 'mr-3': !hasDelete}"
/>
<BaseButton
:key="'delete'+user.id"
v-if="hasDelete"
@click="remove(user)"
class="remove-assignee"
>
<icon icon="times"/>
</BaseButton>
</span>
</div>
</template>
<style scoped lang="scss">
.assignees-list {
display: flex;
&.is-inline :deep(.user) {
display: inline;
}
&:hover .assignee:not(:first-child) {
margin-left: -1rem;
}
}
.assignee {
position: relative;
transition: all $transition;
&:not(:first-child) {
margin-left: -1.5rem;
}
:deep(.user img) {
border: 2px solid var(--white);
margin-right: 0;
}
}
.remove-assignee {
position: absolute;
top: 4px;
left: 2px;
color: var(--danger);
background: var(--white);
padding: 0 4px;
display: block;
border-radius: 100%;
font-size: .75rem;
width: 18px;
height: 18px;
z-index: 100;
}
</style>

View File

@ -37,8 +37,8 @@
>
<div class="filename">
{{ a.file.name }}
<span
v-if="task.coverImageAttachmentId === a.id"
<span
v-if="task.coverImageAttachmentId === a.id"
class="is-task-cover"
>
{{ $t('task.attachment.usedAsCover') }}
@ -115,18 +115,20 @@
</x-button>
<!-- Dropzone -->
<div
:class="{ hidden: !isOverDropZone }"
class="dropzone"
v-if="editEnabled"
>
<div class="drop-hint">
<div class="icon">
<icon icon="cloud-upload-alt"/>
<Teleport to="body">
<div
:class="{ hidden: !isOverDropZone }"
class="dropzone"
v-if="editEnabled"
>
<div class="drop-hint">
<div class="icon">
<icon icon="cloud-upload-alt"/>
</div>
<div class="hint">{{ $t('task.attachment.drop') }}</div>
</div>
<div class="hint">{{ $t('task.attachment.drop') }}</div>
</div>
</div>
</Teleport>
<!-- Delete modal -->
<modal
@ -323,7 +325,7 @@ async function setCoverImage(attachment: IAttachment | null) {
.dropzone {
position: fixed;
background: rgba(250, 250, 250, 0.8);
background: hsla(var(--grey-100-hsl), 0.8);
top: 0;
left: 0;
bottom: 0;
@ -356,11 +358,11 @@ async function setCoverImage(attachment: IAttachment | null) {
.hint {
margin: .5rem auto 2rem;
border-radius: 2px;
border-radius: $radius;
box-shadow: var(--shadow-md);
background: var(--primary);
padding: 1rem;
color: var(--white);
color: $white; // Should always be white because of the background, regardless of the theme
width: 100%;
max-width: 300px;
}

View File

@ -66,7 +66,6 @@
</CustomTransition>
</div>
<editor
:hasPreview="true"
:is-edit-enabled="canWrite && c.author.id === currentUserId"
:upload-callback="attachmentUpload"
:upload-enabled="true"
@ -83,6 +82,7 @@
}"
:bottom-actions="actions[c.id]"
:show-save="true"
initial-mode="preview"
/>
</div>
</div>
@ -114,12 +114,12 @@
taskCommentService.loading &&
!isCommentEdit,
}"
:hasPreview="false"
:upload-callback="attachmentUpload"
:upload-enabled="true"
:placeholder="$t('task.comment.placeholder')"
v-if="editorActive"
v-model="newComment.comment"
@save="addComment()"
/>
</div>
<div class="field">

View File

@ -18,46 +18,43 @@
</h3>
<editor
:is-edit-enabled="canWrite"
:upload-callback="attachmentUpload"
:upload-enabled="true"
:upload-callback="uploadCallback"
:placeholder="$t('task.description.placeholder')"
:empty-text="$t('task.description.empty')"
:show-save="true"
edit-shortcut="e"
v-model="task.description"
v-model="description"
@update:model-value="saveWithDelay"
@save="save"
:initial-mode="isEditorContentEmpty(description) ? 'preview' : 'edit'"
/>
</div>
</template>
<script setup lang="ts">
import {ref, computed, watch, type PropType} from 'vue'
import {ref, computed, watch} from 'vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks'
import TaskModel from '@/models/task'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
const props = defineProps({
modelValue: {
type: Object as PropType<ITask>,
required: true,
},
attachmentUpload: {
required: true,
},
canWrite: {
type: Boolean,
required: true,
},
})
type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
const {
modelValue,
attachmentUpload,
canWrite,
} = defineProps<{
modelValue: ITask,
attachmentUpload: AttachmentUploadFunction,
canWrite: boolean,
}>()
const emit = defineEmits(['update:modelValue'])
const task = ref<ITask>(new TaskModel())
const description = ref<string>('')
const saved = ref(false)
// Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
@ -67,9 +64,9 @@ const taskStore = useTaskStore()
const loading = computed(() => taskStore.isLoading)
watch(
props.modelValue,
(value) => {
task.value = value
() => modelValue.description,
value => {
description.value = value
},
{immediate: true},
)
@ -95,8 +92,11 @@ async function save() {
try {
// FIXME: don't update state from internal.
task.value = await taskStore.update(task.value)
emit('update:modelValue', task.value)
const updated = await taskStore.update({
...modelValue,
description: description.value,
})
emit('update:modelValue', updated)
saved.value = true
setTimeout(() => {
@ -106,5 +106,20 @@ async function save() {
saving.value = false
}
}
async function uploadCallback(files: File[] | FileList): (Promise<string[]>) {
const uploadPromises: Promise<string>[] = []
files.forEach((file: File) => {
const promise = new Promise<string>((resolve) => {
attachmentUpload(file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
})
uploadPromises.push(promise)
})
return await Promise.all(uploadPromises)
}
</script>

View File

@ -11,13 +11,8 @@
v-model="assignees"
:autocomplete-enabled="false"
>
<template #tag="{item: user}">
<span class="assignee">
<user :avatar-size="32" :show-username="false" :user="user" class="m-2"/>
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
<icon icon="times"/>
</BaseButton>
</span>
<template #items="{items}">
<assignee-list :assignees="items" :remove="removeAssignee"/>
</template>
<template #searchResult="{option: user}">
<user :avatar-size="24" :show-username="true" :user="user"/>
@ -31,7 +26,6 @@ import {useI18n} from 'vue-i18n'
import User from '@/components/misc/user.vue'
import Multiselect from '@/components/input/multiselect.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {includesById} from '@/helpers/utils'
import ProjectUserService from '@/services/projectUsers'
@ -40,6 +34,7 @@ import {useTaskStore} from '@/stores/tasks'
import type {IUser} from '@/modelTypes/IUser'
import {getDisplayName} from '@/models/user'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const props = defineProps({
taskId: {
@ -120,34 +115,3 @@ async function findUser(query: string) {
})
}
</script>
<style lang="scss" scoped>
.assignee {
position: relative;
&:not(:first-child) {
margin-left: -1.5rem;
}
:deep(.user img) {
border: 2px solid var(--white);
margin-right: 0;
}
}
.remove-assignee {
position: absolute;
top: 4px;
left: 2px;
color: var(--danger);
background: var(--white);
padding: 0 4px;
display: block;
border-radius: 100%;
font-size: .75rem;
width: 18px;
height: 18px;
z-index: 100;
}
</style>

View File

@ -113,7 +113,7 @@ async function save(title: string) {
<style lang="scss" scoped>
.heading {
display: flex;
justify-content: space-between;
justify-content: flex-start;
text-transform: none;
align-items: center;
@ -134,6 +134,10 @@ async function save(title: string) {
@media screen and (max-width: $tablet) {
margin: 0 -.3rem .5rem -.3rem; // the title has 0.3rem padding - this make the text inside of it align with the rest
}
@media screen and (min-width: $tablet) and (max-width: #{$desktop + $close-button-min-space}) {
width: calc(100% - 6.5rem);
}
}
.title.task-id {

View File

@ -48,16 +48,14 @@
</progress>
<div class="footer">
<labels :labels="task.labels"/>
<priority-label :priority="task.priority" :done="task.done"/>
<div class="assignees" v-if="task.assignees.length > 0">
<user
v-for="u in task.assignees"
:avatar-size="24"
:key="task.id + 'assignee' + u.id"
:show-username="false"
:user="u"
/>
</div>
<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"
:avatar-size="24"
class="ml-1"
:inline="true"
/>
<checklist-summary :task="task"/>
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
@ -78,7 +76,6 @@ import {ref, computed, watch} from 'vue'
import {useRouter} from 'vue-router'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import User from '@/components/misc/user.vue'
import Done from '@/components/misc/Done.vue'
import Labels from '@/components/tasks/partials/labels.vue'
import ChecklistSummary from './checklist-summary.vue'
@ -91,6 +88,9 @@ import AttachmentService from '@/services/attachment'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop'
const router = useRouter()
@ -109,10 +109,14 @@ const color = computed(() => getHexColor(task.hexColor))
async function toggleTaskDone(task: ITask) {
loadingInternal.value = true
try {
await useTaskStore().update({
const updatedTask = await useTaskStore().update({
...task,
done: !task.done,
})
if (updatedTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
} finally {
loadingInternal.value = false
}
@ -238,7 +242,7 @@ $task-background: var(--white);
.priority-label {
font-size: .75rem;
height: 2rem;
padding: 0 .5rem 0 .25rem;
.icon {
height: 1rem;

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import type {ILabel} from '@/modelTypes/ILabel'
defineProps<{
label: ILabel
}>()
</script>
<template>
<span
:key="label.id"
:style="{'background': label.hexColor, 'color': label.textColor}"
class="tag"
>
<span>{{ label.title }}</span>
</span>
</template>
<style scoped lang="scss">
.tag {
& + & {
margin-left: 0.5rem;
}
}
</style>

View File

@ -1,12 +1,10 @@
<template>
<div class="label-wrapper">
<span
<XLabel
v-for="label in labels"
:label="label"
:key="label.id"
:style="{'background': label.hexColor, 'color': label.textColor}"
class="tag"
v-for="label in labels">
<span>{{ label.title }}</span>
</span>
/>
</div>
</template>
@ -14,6 +12,8 @@
import type {PropType} from 'vue'
import type {ILabel} from '@/modelTypes/ILabel'
import XLabel from '@/components/tasks/partials/label.vue'
defineProps({
labels: {
type: Array as PropType<ILabel[]>,
@ -26,10 +26,4 @@ defineProps({
.label-wrapper {
display: inline;
}
.tag {
& + & {
margin-left: 0.5rem;
}
}
</style>

View File

@ -14,7 +14,7 @@
<template v-if="priority === priorities.URGENT">{{ $t('task.priority.urgent') }}</template>
<template v-if="priority === priorities.DO_NOW">{{ $t('task.priority.doNow') }}</template>
</span>
<span class="icon" v-if="priority === priorities.DO_NOW">
<span class="icon pr-0" v-if="priority === priorities.DO_NOW">
<icon icon="exclamation"/>
</span>
</span>
@ -40,17 +40,12 @@ defineProps({
</script>
<style lang="scss" scoped>
.priority-label {
display: inline-flex;
align-items: center;
}
span.high-priority {
color: var(--danger);
width: auto !important; // To override the width set in tasks
.icon {
vertical-align: middle;
vertical-align: top;
width: auto !important;
padding: 0 .5rem;
}

View File

@ -70,11 +70,11 @@ function findProjects(query: string) {
foundProjects.value = projectStore.searchProject(query)
}
function select(l: IProject | null) {
if (l === null) {
return
function select(p: IProject | null) {
if (p === null) {
Object.assign(project, {id: 0})
}
Object.assign(project, l)
Object.assign(project, p)
emit('update:modelValue', project)
}
</script>

View File

@ -42,7 +42,7 @@
{{ $t('task.quickAddMagic.multiple') }}
</p>
<h3>{{ $t('project.list.title') }}</h3>
<h3>{{ $t('quickActions.projects') }}</h3>
<p>
{{ $t('task.quickAddMagic.project1', {prefix: prefixes.project}) }}
{{ $t('task.quickAddMagic.project2') }}
@ -108,7 +108,7 @@ const visible = ref(false)
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
defineProps<{
highlightHintIcon: boolean,
highlightHintIcon?: boolean,
}>()
const prefixes = computed(() => PREFIXES[mode.value])

View File

@ -9,6 +9,7 @@
variant="secondary"
icon="plus"
:shadow="false"
id="showRelatedTasksFormButton"
/>
<transition-group name="fade">
<template v-if="editEnabled && showCreate">
@ -35,6 +36,7 @@
:creatable="true"
:create-placeholder="$t('task.relation.createPlaceholder')"
@create="createAndRelateTask"
v-focus
>
<template #searchResult="{option: task}">
<span
@ -161,6 +163,8 @@ import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import {useProjectStore} from '@/stores/projects'
import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop'
const props = defineProps({
taskId: {
@ -329,6 +333,10 @@ async function createAndRelateTask(title: string) {
async function toggleTaskDone(task: ITask) {
await taskStore.update(task)
if (task.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
// Find the task in the project and update it so that it is correctly strike through
Object.entries(relatedTasks.value).some(([kind, tasks]) => {
return (tasks as ITask[]).some((t, key) => {

View File

@ -7,8 +7,8 @@
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">
{{ $t('task.repeat.everyWeek') }}
</x-button>
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">
{{ $t('task.repeat.everyMonth') }}
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(30, 'days')">
{{ $t('task.repeat.every30d') }}
</x-button>
</div>
<div class="is-flex is-align-items-center mb-2">
@ -51,8 +51,6 @@
<option value="hours">{{ $t('task.repeat.hours') }}</option>
<option value="days">{{ $t('task.repeat.days') }}</option>
<option value="weeks">{{ $t('task.repeat.weeks') }}</option>
<option value="months">{{ $t('task.repeat.months') }}</option>
<option value="years">{{ $t('task.repeat.years') }}</option>
</select>
</div>
</div>

View File

@ -1,29 +1,34 @@
<template>
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
@update:model-value="markAsDone"
v-model="task.done"
/>
<ColorBubble
v-if="showProjectColor && projectColor !== '' && currentProject?.id !== task.projectId"
:color="projectColor"
class="mr-1"
/>
<router-link
:to="taskDetailRoute"
:class="{ 'done': task.done, 'show-project': showProject && project}"
class="tasktext"
<div>
<div
:class="{'is-loading': taskService.loading}"
class="task loader-container"
@click.stop.self="openTaskDetail"
>
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
@update:model-value="markAsDone"
v-model="task.done"
/>
<ColorBubble
v-if="!showProjectSeparately && projectColor !== '' && currentProject?.id !== task.projectId"
:color="projectColor"
class="mr-1"
/>
<div
:class="{ 'done': task.done, 'show-project': showProject && project}"
class="tasktext"
>
<span>
<router-link
v-if="showProject && typeof project !== 'undefined'"
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-project"
class="task-project mr-1"
:class="{'mr-2': task.hexColor !== ''}"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})">
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
>
{{ project.title }}
</router-link>
@ -32,56 +37,54 @@
:color="getHexColor(task.hexColor)"
class="mr-1"
/>
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
<span class="parent-tasks" v-if="typeof task.relatedTasks?.parenttask !== 'undefined'">
<template v-for="(pt, i) in task.relatedTasks.parenttask">
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">,&nbsp;</template>
</template>
&rsaquo;
</span>
{{ task.title }}
<priority-label :priority="task.priority" :done="task.done" class="pr-2"/>
<router-link
:to="taskDetailRoute"
class="task-link"
ref="taskLink"
tabindex="-1"
>
{{ task.title }}
</router-link>
</span>
<labels
v-if="task.labels.length > 0"
class="labels ml-2 mr-1"
:labels="task.labels"
/>
<labels
v-if="task.labels.length > 0"
class="labels ml-2 mr-1"
:labels="task.labels"
/>
<User
v-for="(a, i) in task.assignees"
:avatar-size="27"
:is-inline="true"
:key="task.id + 'assignee' + a.id + i"
:show-username="false"
:user="a"
class="m-2"
/>
<assignee-list
v-if="task.assignees.length > 0"
:assignees="task.assignees"
:avatar-size="25"
class="ml-1"
:inline="true"
/>
<!-- FIXME: use popup -->
<BaseButton
v-if="+new Date(task.dueDate) > 0"
class="dueDate"
@click.prevent.stop="showDefer = !showDefer"
v-tooltip="formatDateLong(task.dueDate)"
>
<time
:datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
:aria-expanded="showDefer ? 'true' : 'false'"
<!-- FIXME: use popup -->
<BaseButton
v-if="+new Date(task.dueDate) > 0"
class="dueDate"
@click.prevent.stop="showDefer = !showDefer"
v-tooltip="formatDateLong(task.dueDate)"
>
{{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</time>
</BaseButton>
<CustomTransition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</CustomTransition>
<time
:datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
:aria-expanded="showDefer ? 'true' : 'false'"
>
{{ $t('task.detail.due', {at: dueDateFormatted}) }}
</time>
</BaseButton>
<CustomTransition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</CustomTransition>
<priority-label :priority="task.priority" :done="task.done"/>
<span>
<span>
<span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
@ -93,35 +96,56 @@
</span>
</span>
<checklist-summary :task="task"/>
</router-link>
<checklist-summary :task="task"/>
</div>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100"
>
{{ task.percentDone * 100 }}%
</progress>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100"
>
{{ task.percentDone * 100 }}%
</progress>
<router-link
v-if="!showProject && currentProject?.id !== task.projectId && project"
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-project"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
>
{{ project.title }}
</router-link>
<ColorBubble
v-if="showProjectSeparately && projectColor !== '' && currentProject?.id !== task.projectId"
:color="projectColor"
class="mr-1"
/>
<router-link
v-if="showProjectSeparately"
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-project"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
>
{{ project.title }}
</router-link>
<BaseButton
:class="{'is-favorite': task.isFavorite}"
@click="toggleFavorite"
class="favorite"
>
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</BaseButton>
<slot />
<BaseButton
:class="{'is-favorite': task.isFavorite}"
@click="toggleFavorite"
class="favorite"
>
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</BaseButton>
<slot/>
</div>
<template v-if="typeof task.relatedTasks?.subtask !== 'undefined'">
<template v-for="subtask in task.relatedTasks.subtask">
<template v-if="getTaskById(subtask.id)">
<single-task-in-project
:key="subtask.id"
:the-task="getTaskById(subtask.id)"
:disabled="disabled"
:can-mark-as-done="canMarkAsDone"
:all-tasks="allTasks"
class="subtask-nested"
/>
</template>
</template>
</template>
</div>
</template>
@ -129,7 +153,7 @@
import {ref, watch, shallowReactive, onMounted, onBeforeUnmount, computed} from 'vue'
import {useI18n} from 'vue-i18n'
import TaskModel, { getHexColor } from '@/models/task'
import TaskModel, {getHexColor} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
@ -137,7 +161,6 @@ import Labels from '@/components/tasks/partials//labels.vue'
import DeferTask from '@/components/tasks/partials//defer-task.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
import User from '@/components/misc/user.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
@ -152,23 +175,35 @@ import {success} from '@/message'
import {useProjectStore} from '@/stores/projects'
import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import {useIntervalFn} from '@vueuse/core'
import {playPopSound} from '@/helpers/playPop'
import {useAuthStore} from '@/stores/auth'
const {
theTask,
isArchived = false,
showProject = false,
disabled = false,
showProjectColor = false,
canMarkAsDone = true,
allTasks = [],
} = defineProps<{
theTask: ITask,
isArchived?: boolean,
showProject?: boolean,
disabled?: boolean,
showProjectColor?: boolean,
canMarkAsDone?: boolean,
allTasks?: ITask[],
}>()
function getTaskById(taskId: number): ITask | undefined {
if (typeof allTasks === 'undefined' || allTasks.length === 0) {
return null
}
return allTasks.find(t => t.id === taskId)
}
const emit = defineEmits(['task-updated'])
const {t} = useI18n({useScope: 'global'})
@ -200,6 +235,8 @@ const taskStore = useTaskStore()
const project = computed(() => projectStore.projects[task.value.projectId])
const projectColor = computed(() => project.value ? project.value?.hexColor : '')
const showProjectSeparately = computed(() => !showProject && currentProject.value?.id !== task.value.projectId && project.value)
const currentProject = computed(() => {
return typeof baseStore.currentProject === 'undefined' ? {
id: 0,
@ -214,11 +251,28 @@ const taskDetailRoute = computed(() => ({
// state: { backdropView: router.currentRoute.value.fullPath },
}))
function updateDueDate() {
if (!task.value.dueDate) {
return
}
dueDateFormatted.value = formatDateSince(task.value.dueDate)
}
const dueDateFormatted = ref('')
useIntervalFn(updateDueDate, 60_000, {
immediateCallback: true,
})
onMounted(updateDueDate)
async function markAsDone(checked: boolean) {
const updateFunc = async () => {
const newTask = await taskStore.update(task.value)
task.value = newTask
if (checked && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
emit('task-updated', newTask)
success({
message: task.value.done ?
@ -248,6 +302,7 @@ async function toggleFavorite() {
}
const deferDueDate = ref<typeof DeferTask | null>(null)
function hideDeferDueDatePopup(e) {
if (!showDefer.value) {
return
@ -256,6 +311,15 @@ function hideDeferDueDatePopup(e) {
showDefer.value = false
})
}
const taskLink = ref<HTMLElement | null>(null)
function openTaskDetail() {
const isTextSelected = window.getSelection().toString()
if (!isTextSelected) {
taskLink.value.$el.click()
}
}
</script>
<style lang="scss" scoped>
@ -273,6 +337,14 @@ function hideDeferDueDatePopup(e) {
background-color: var(--grey-100);
}
&:focus-within, &:focus {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
a.task-link {
box-shadow: none;
}
}
.tasktext,
&.tasktext {
text-overflow: ellipsis;
@ -283,7 +355,7 @@ function hideDeferDueDatePopup(e) {
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1 0 50%;
.dueDate {
@ -364,6 +436,9 @@ function hideDeferDueDatePopup(e) {
}
}
.favorite:focus {
opacity: 1;
}
:deep(.fancycheckbox) {
height: 18px;
@ -393,7 +468,7 @@ function hideDeferDueDatePopup(e) {
color: var(--danger);
}
input[type="checkbox"] {
input[type='checkbox'] {
vertical-align: middle;
}
@ -416,4 +491,8 @@ function hideDeferDueDatePopup(e) {
margin-bottom: 0;
}
}
.subtask-nested {
margin-left: 1.75rem;
}
</style>

View File

@ -0,0 +1,190 @@
<template>
<div class="task">
<span>
<span
v-if="showProject && typeof project !== 'undefined'"
class="task-project"
:class="{'mr-2': task.hexColor !== ''}"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
>
{{ project.title }}
</span>
<ColorBubble
v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)"
class="mr-1"
/>
<priority-label :priority="task.priority" :done="task.done"/>
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
<span class="parent-tasks" v-if="typeof task.relatedTasks?.parenttask !== 'undefined'">
<template v-for="(pt, i) in task.relatedTasks.parenttask">
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">,&nbsp;</template>
</template>
&rsaquo;
</span>
{{ task.title }}
</span>
<labels
v-if="task.labels.length > 0"
class="labels ml-2 mr-1"
:labels="task.labels"
/>
<assignee-list
v-if="task.assignees.length > 0"
:assignees="task.assignees"
:avatar-size="20"
class="ml-1"
:inline="true"
/>
<span
v-if="+new Date(task.dueDate) > 0"
class="dueDate"
v-tooltip="formatDateLong(task.dueDate)"
>
<time
:datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
>
{{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</time>
</span>
<span>
<span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span class="project-task-icon" v-if="task.description">
<icon icon="align-left"/>
</span>
<span class="project-task-icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
</span>
</span>
<checklist-summary :task="task"/>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100"
>
{{ task.percentDone * 100 }}%
</progress>
</div>
</template>
<script setup lang="ts">
import {computed} from 'vue'
import {getHexColor} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import Labels from '@/components/tasks/partials//labels.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import {useProjectStore} from '@/stores/projects'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const {
task,
showProject = false,
} = defineProps<{
task: ITask,
showProject?: boolean,
}>()
const projectStore = useProjectStore()
const project = computed(() => projectStore.projects[task.projectId])
</script>
<style lang="scss" scoped>
.task {
display: flex;
flex-wrap: wrap;
transition: background-color $transition;
align-items: center;
cursor: pointer;
border-radius: $radius;
border: 2px solid transparent;
text-overflow: ellipsis;
word-wrap: break-word;
word-break: break-word;
//display: -webkit-box;
hyphens: auto;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
//flex: 1 0 50%;
.dueDate {
display: inline-block;
margin-left: 5px;
}
.overdue {
color: var(--danger);
}
.task-project {
width: auto;
color: var(--grey-400);
font-size: .9rem;
white-space: nowrap;
}
.avatar {
border-radius: 50%;
vertical-align: bottom;
margin-left: .5rem;
height: 21px;
width: 21px;
}
.project-task-icon {
margin-left: 6px;
&:not(:first-of-type) {
margin-left: 8px;
}
}
a {
color: var(--text);
transition: color ease $transition-duration;
&:hover {
color: var(--grey-900);
}
}
.tasktext.done {
text-decoration: line-through;
color: var(--grey-500);
}
span.parent-tasks {
color: var(--grey-500);
width: auto;
}
.progress {
margin-bottom: 0;
}
}
</style>

View File

@ -1,7 +1,8 @@
import {computed, ref, watch, type Ref} from 'vue'
import {useRouter, type RouteLocationNormalized, type RouteLocationRaw} from 'vue-router'
import {useRouter, type RouteLocationNormalized, type RouteLocationRaw, type RouteRecordName} from 'vue-router'
import equal from 'fast-deep-equal/es6'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Filters = Record<string, any>
export function useRouteFilters<CurrentFilters extends Filters>(
@ -9,6 +10,7 @@ export function useRouteFilters<CurrentFilters extends Filters>(
getDefaultFilters: (route: RouteLocationNormalized) => CurrentFilters,
routeToFilters: (route: RouteLocationNormalized) => CurrentFilters,
filtersToRoute: (filters: CurrentFilters) => RouteLocationRaw,
routeAllowList: RouteRecordName[] = [],
) {
const router = useRouter()
@ -17,11 +19,12 @@ export function useRouteFilters<CurrentFilters extends Filters>(
const routeFromFiltersFullPath = computed(() => router.resolve(filtersToRoute(filters.value)).fullPath)
watch(
route,
route.value,
(route, oldRoute) => {
if (
route?.name !== oldRoute?.name ||
routeFromFiltersFullPath.value === route.fullPath
routeFromFiltersFullPath.value === route.fullPath ||
!routeAllowList.includes(route.name ?? '')
) {
return
}

View File

@ -24,13 +24,23 @@ export function useRouteWithModal() {
// this is adapted from vue-router
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
const routePropsOption = route.matched[0]?.props.default
const routeProps = routePropsOption
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: {}
let routeProps = undefined
if (routePropsOption) {
if (routePropsOption === true) {
routeProps = route.params
} else {
if(typeof routePropsOption === 'function') {
routeProps = routePropsOption(route)
} else {
routeProps = routePropsOption
}
}
}
if (typeof routeProps === 'undefined') {
currentModal.value = undefined
return
}
routeProps.backdropView = backdropView.value

View File

@ -1,5 +1,6 @@
import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
import {useRoute} from 'vue-router'
import {useRouteQuery} from '@vueuse/router'
import TaskCollectionService from '@/services/taskCollection'
import type {ITask} from '@/modelTypes/ITask'
@ -68,23 +69,33 @@ export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sor
const params = ref({...getDefaultParams()})
const search = ref('')
const page = ref(1)
const page = useRouteQuery('page', '1', { transform: Number })
const sortBy = ref({ ...sortByDefault })
const getAllTasksParams = computed(() => {
let loadParams = {...params.value}
const allParams = computed(() => {
const loadParams = {...params.value}
if (search.value !== '') {
loadParams.s = search.value
}
loadParams = formatSortOrder(sortBy.value, loadParams)
return formatSortOrder(sortBy.value, loadParams)
})
watch(
() => allParams.value,
() => {
// When parameters change, the page should always be the first
page.value = 1
},
)
const getAllTasksParams = computed(() => {
return [
{projectId: projectId.value},
loadParams,
page.value || 1,
allParams.value,
page.value,
]
})

View File

@ -1,15 +1,11 @@
import type {Directive} from 'vue'
import {install, uninstall} from '@github/hotkey'
import {isAppleDevice} from '@/helpers/isAppleDevice'
const directive = <Directive<HTMLElement,string>>{
mounted(el, {value}) {
if(value === '') {
return
}
if (isAppleDevice() && value.includes('Control')) {
value = value.replace('Control', 'Meta')
}
install(el, value)
},
beforeUnmount(el) {

View File

@ -4,6 +4,7 @@ import {snakeCase} from 'snake-case'
/**
* Transforms field names to camel case.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function objectToCamelCase(object: Record<string, any>) {
// When calling recursively, this can be called without being and object or array in which case we just return the value
@ -11,6 +12,7 @@ export function objectToCamelCase(object: Record<string, any>) {
return object
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parsedObject: Record<string, any> = {}
for (const m in object) {
parsedObject[camelCase(m)] = object[m]
@ -23,6 +25,7 @@ export function objectToCamelCase(object: Record<string, any>) {
// Call it again for arrays
if (Array.isArray(object[m])) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parsedObject[camelCase(m)] = object[m].map((o: Record<string, any>) => objectToCamelCase(o))
// Because typeof [] === 'object' is true for arrays, we leave the loop here to prevent converting arrays to objects.
continue
@ -39,6 +42,7 @@ export function objectToCamelCase(object: Record<string, any>) {
/**
* Transforms field names to snake case - used before making an api request.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function objectToSnakeCase(object: Record<string, any>) {
// When calling recursively, this can be called without being and object or array in which case we just return the value
@ -46,6 +50,7 @@ export function objectToSnakeCase(object: Record<string, any>) {
return object
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parsedObject: Record<string, any> = {}
for (const m in object) {
parsedObject[snakeCase(m)] = object[m]
@ -61,6 +66,7 @@ export function objectToSnakeCase(object: Record<string, any>) {
// Call it again for arrays
if (Array.isArray(object[m])) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parsedObject[snakeCase(m)] = object[m].map((o: Record<string, any>) => objectToSnakeCase(o))
// Because typeof [] === 'object' is true for arrays, we leave the loop here to prevent converting arrays to objects.
continue

View File

@ -4,8 +4,27 @@ const API_DEFAULT_PORT = '3456'
export const ERROR_NO_API_URL = 'noApiUrlProvided'
export class NoApiUrlProvidedError extends Error {
constructor() {
super()
this.message = 'No API URL provided'
this.name = 'NoApiUrlProvidedError'
}
}
export class InvalidApiUrlProvidedError extends Error {
constructor() {
super()
this.message = 'The provided API URL is invalid.'
this.name = 'InvalidApiUrlProvidedError'
}
}
export const checkAndSetApiUrl = (url: string | undefined | null): Promise<string> => {
if (url === '' || url === null || typeof url === 'undefined') {
throw new NoApiUrlProvidedError()
}
export const checkAndSetApiUrl = (url: string): Promise<string> => {
if (url.startsWith('/')) {
url = window.location.host + url
}
@ -17,8 +36,14 @@ export const checkAndSetApiUrl = (url: string): Promise<string> => {
) {
url = `${window.location.protocol}//${url}`
}
let urlToCheck: URL
try {
urlToCheck = new URL(url)
} catch (e) {
throw new InvalidApiUrlProvidedError()
}
const urlToCheck: URL = new URL(url)
const origUrlToCheck = urlToCheck
const oldUrl = window.API_URL
@ -86,6 +111,6 @@ export const checkAndSetApiUrl = (url: string): Promise<string> => {
return window.API_URL
}
throw new Error(ERROR_NO_API_URL)
throw new InvalidApiUrlProvidedError()
})
}

View File

@ -10,48 +10,56 @@ describe('Find checklists in text', () => {
expect(checkboxes).toHaveLength(0)
})
it('should find multiple checkboxes', () => {
const text: string = `* [ ] Lorem Ipsum
* [ ] Dolor sit amet
Here's some text in between
* [x] Dolor sit amet
- [ ] Dolor sit amet`
const text: string = `
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Task</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Another task</p>
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>subtask</p></div>
</li>
<li data-checked="true" data-type="taskItem"><label><input type="checkbox"
checked="checked"><span></span></label>
<div><p>done</p></div>
</li>
</ul>
</div>
</li>
</ul>`
const checkboxes = findCheckboxesInText(text)
expect(checkboxes).toHaveLength(4)
expect(checkboxes[0]).toBe(0)
expect(checkboxes[1]).toBe(18)
expect(checkboxes[2]).toBe(69)
expect(checkboxes[3]).toBe(90)
expect(checkboxes[0]).toBe(32)
expect(checkboxes[1]).toBe(163)
expect(checkboxes[2]).toBe(321)
expect(checkboxes[3]).toBe(464)
})
it('should find one checkbox with *', () => {
const text: string = '* [ ] Lorem Ipsum'
it('should find one unchecked checkbox', () => {
const text: string = `
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Task</p></div>
</li>
</ul>`
const checkboxes = findCheckboxesInText(text)
expect(checkboxes).toHaveLength(1)
expect(checkboxes[0]).toBe(0)
expect(checkboxes[0]).toBe(32)
})
it('should find one checkbox with -', () => {
const text: string = '- [ ] Lorem Ipsum'
it('should find one checked checkbox', () => {
const text: string = `
<ul data-type="taskList">
<li data-checked="true" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Task</p></div>
</li>
</ul>`
const checkboxes = findCheckboxesInText(text)
expect(checkboxes).toHaveLength(1)
expect(checkboxes[0]).toBe(0)
})
it('should find one checked checkbox with *', () => {
const text: string = '* [x] Lorem Ipsum'
const checkboxes = findCheckboxesInText(text)
expect(checkboxes).toHaveLength(1)
expect(checkboxes[0]).toBe(0)
})
it('should find one checked checkbox with -', () => {
const text: string = '- [x] Lorem Ipsum'
const checkboxes = findCheckboxesInText(text)
expect(checkboxes).toHaveLength(1)
expect(checkboxes[0]).toBe(0)
expect(checkboxes[0]).toBe(32)
})
})
@ -63,32 +71,60 @@ describe('Get Checklist Statistics in a Text', () => {
expect(stats.total).toBe(0)
})
it('should find one checkbox', () => {
const text: string = '* [ ] Lorem Ipsum'
const text: string = `
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Task</p></div>
</li>
</ul>`
const stats = getChecklistStatistics(text)
expect(stats.total).toBe(1)
expect(stats.checked).toBe(0)
})
it('should find one checked checkbox', () => {
const text: string = '* [x] Lorem Ipsum'
const text: string = `
<ul data-type="taskList">
<li data-checked="true" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Task</p></div>
</li>
</ul>`
const stats = getChecklistStatistics(text)
expect(stats.total).toBe(1)
expect(stats.checked).toBe(1)
})
it('should find multiple mixed and matched', () => {
const text: string = `* [ ] Lorem Ipsum
* [ ] Dolor sit amet
* [x] Dolor sit amet
- [x] Dolor sit amet
const text: string = `
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Task</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>Another task</p>
<ul data-type="taskList">
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>subtask</p></div>
</li>
<li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label>
<div><p>subtask 2</p></div>
</li>
<li data-checked="true" data-type="taskItem"><label><input type="checkbox"
checked="checked"><span></span></label>
<div><p>done</p></div>
</li>
<li data-checked="true" data-type="taskItem"><label><input type="checkbox"
checked="checked"><span></span></label>
<div><p>also done</p></div>
</li>
</ul>
</div>
</li>
</ul>`
Here's some text in between
* [x] Dolor sit amet
- [ ] Dolor sit amet`
const stats = getChecklistStatistics(text)
expect(stats.total).toBe(6)
expect(stats.checked).toBe(3)
expect(stats.checked).toBe(2)
})
})

View File

@ -1,5 +1,3 @@
const checked = '[x]'
interface CheckboxStatistics {
total: number
checked: number
@ -11,7 +9,7 @@ interface MatchedCheckboxes {
}
const getCheckboxesInText = (text: string): MatchedCheckboxes => {
const regex = /[*-] \[[ x]]/g
const regex = /data-checked="(true|false)"/g
let match
const checkboxes: MatchedCheckboxes = {
checked: [],
@ -19,7 +17,7 @@ const getCheckboxesInText = (text: string): MatchedCheckboxes => {
}
while ((match = regex.exec(text)) !== null) {
if (match[0].endsWith(checked)) {
if (match[1] === 'true') {
checkboxes.checked.push(match.index)
} else {
checkboxes.unchecked.push(match.index)

View File

@ -0,0 +1,20 @@
const COLORS = [
'#ffbe0b',
'#fd8a09',
'#fb5607',
'#ff006e',
'#efbdeb',
'#8338ec',
'#5f5ff6',
'#3a86ff',
'#4c91ff',
'#0ead69',
'#25be8b',
'#073b4c',
'#373f47',
]
export function getRandomColorHex(): string {
return COLORS[Math.floor(Math.random() * COLORS.length)]
}

View File

@ -0,0 +1,3 @@
export function isEditorContentEmpty(content: string): boolean {
return content === '' || content === '<p></p>'
}

View File

@ -0,0 +1,11 @@
export function isValidHttpUrl(urlToCheck: string): boolean {
let url
try {
url = new URL(urlToCheck)
} catch (_) {
return false
}
return url.protocol === 'http:' || url.protocol === 'https:'
}

View File

@ -1,45 +0,0 @@
import {marked} from 'marked'
import hljs from 'highlight.js/lib/common'
export function setupMarkdownRenderer(checkboxId: string) {
const renderer = new marked.Renderer()
const linkRenderer = renderer.link
let checkboxNum = -1
marked.use({
renderer: {
image(src: string, title: string, text: string) {
title = title ? ` title="${title}` : ''
// If the url starts with the api url, the image is likely an attachment and
// we'll need to download and parse it properly.
if (src.slice(0, window.API_URL.length + 7) === `${window.API_URL}/tasks/`) {
return `<img data-src="${src}" alt="${text}" ${title} class="attachment-image"/>`
}
return `<img src="${src}" alt="${text}" ${title}/>`
},
checkbox(checked: boolean) {
let checkedString = ''
if (checked) {
checkedString = 'checked'
}
checkboxNum++
return `<input type="checkbox" data-checkbox-num="${checkboxNum}" ${checkedString} class="text-checkbox-${checkboxId}"/>`
},
link(href: string, title: string, text: string) {
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
const html = linkRenderer.call(renderer, href, title, text)
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
},
},
highlight(code: string, language: string) {
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
return hljs.highlight(code, {language: validLanguage}).value
},
})
return renderer
}

View File

@ -4,7 +4,7 @@ import {PrefixMode} from '@/modules/parseTaskText'
describe('Parse Subtasks via Relation', () => {
it('Should not return a parent for a single task', () => {
const tasks = parseSubtasksViaIndention('single task')
const tasks = parseSubtasksViaIndention('single task', PrefixMode.Default)
expect(tasks).to.have.length(1)
expect(tasks[0].parent).toBeNull()
@ -118,4 +118,52 @@ task two`, PrefixMode.Default)
expect(tasks[1].project).to.eq('list')
expect(tasks[2].project).to.eq('list')
})
it('Should clean the indention if there is indention on the first line', () => {
const tasks = parseSubtasksViaIndention(
` parent task
sub task one
sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).toBeNull()
expect(tasks[2].title).to.eq('sub task two')
expect(tasks[2].parent).to.eq('sub task one')
})
it('Should clean the indention if there is indention on the first line but not for subsequent tasks', () => {
const tasks = parseSubtasksViaIndention(
` parent task
sub task one
first level task one
sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(4)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).toBeNull()
expect(tasks[2].title).to.eq('first level task one')
expect(tasks[2].parent).toBeNull()
expect(tasks[3].title).to.eq('sub task two')
expect(tasks[3].parent).to.eq('first level task one')
})
it('Should clean the indention if there is indention on the first line for subsequent tasks with less indention', () => {
const tasks = parseSubtasksViaIndention(
` parent task
sub task one
first level task one
sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(4)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).toBeNull()
expect(tasks[2].title).to.eq('first level task one')
expect(tasks[2].parent).toBeNull()
expect(tasks[3].title).to.eq('sub task two')
expect(tasks[3].parent).to.eq('first level task one')
})
})

View File

@ -17,7 +17,31 @@ const spaceRegex = /^ */
* relation between each other.
*/
export function parseSubtasksViaIndention(taskTitles: string, prefixMode: PrefixMode): TaskWithParent[] {
const titles = taskTitles.split(/[\r\n]+/)
let titles = taskTitles
.split(/[\r\n]+/)
.filter(t => t.replace(/\s/g, '').length > 0) // Remove titles which are empty or only contain spaces / tabs
if (titles.length == 0) {
return []
}
const spaceOnFirstLine = /^(\t| )+/
const spaces = spaceOnFirstLine.exec(titles[0])
if (spaces !== null) {
let spacesToCut = spaces[0].length
titles = titles.map(title => {
const spacesOnThisLine = spaceOnFirstLine.exec(title)
if (spacesOnThisLine === null) {
// This means the current task title does not start with indention, but the very first one did
// To prevent cutting actual task data we now need to update the number of spaces to cut
spacesToCut = 0
}
if (spacesOnThisLine !== null && spacesOnThisLine[0].length < spacesToCut) {
spacesToCut = spacesOnThisLine[0].length
}
return title.substring(spacesToCut)
})
}
return titles.map((title, index) => {
const task: TaskWithParent = {
@ -32,7 +56,7 @@ export function parseSubtasksViaIndention(taskTitles: string, prefixMode: Prefix
return task
}
const matched = spaceRegex.exec(title)
const matched = spaceRegex.exec(task.title)
const matchedSpaces = matched ? matched[0].length : 0
if (matchedSpaces > 0) {
@ -45,7 +69,7 @@ export function parseSubtasksViaIndention(taskTitles: string, prefixMode: Prefix
const parentMatched = spaceRegex.exec(task.parent)
parentSpaces = parentMatched ? parentMatched[0].length : 0
} while (parentSpaces >= matchedSpaces)
task.title = cleanupTitle(title.replace(spaceRegex, ''))
task.title = cleanupTitle(task.title.replace(spaceRegex, ''))
task.parent = task.parent.replace(spaceRegex, '')
if (task.project === null) {
// This allows to specify a project once for the parent task and inherit it to all subtasks

View File

@ -1,6 +1,10 @@
const LAST_VISITED_KEY = 'lastVisited'
export const saveLastVisited = (name: string, params: object, query: object) => {
export const saveLastVisited = (name: string | undefined, params: object, query: object) => {
if (typeof name === 'undefined') {
return
}
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
}
@ -9,7 +13,7 @@ export const getLastVisited = () => {
if (lastVisited === null) {
return null
}
return JSON.parse(lastVisited)
}

View File

@ -5,8 +5,8 @@ import {format, formatDistanceToNow} from 'date-fns'
import {enGB, de, fr, ru} from 'date-fns/locale'
import {i18n} from '@/i18n'
import { createSharedComposable, type MaybeRef } from '@vueuse/core'
import { computed, unref } from 'vue'
import {createSharedComposable, type MaybeRef} from '@vueuse/core'
import {computed, unref} from 'vue'
const locales = {en: enGB, de, ch: de, fr, ru}
@ -62,7 +62,7 @@ export const useDateTimeFormatter = createSharedComposable((options?: MaybeRef<I
})
export function useWeekDayFromDate() {
const dateTimeFormatter = useDateTimeFormatter({ weekday: 'short' })
const dateTimeFormatter = useDateTimeFormatter({weekday: 'short'})
return computed(() => (date: Date) => dateTimeFormatter.value.format(date))
}

View File

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

View File

@ -2,9 +2,7 @@ import {
SECONDS_A_DAY,
SECONDS_A_HOUR,
SECONDS_A_MINUTE,
SECONDS_A_MONTH,
SECONDS_A_WEEK,
SECONDS_A_YEAR,
} from '@/constants/date'
export type PeriodUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years'
@ -16,10 +14,6 @@ export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: nu
if (seconds % SECONDS_A_DAY === 0) {
if (seconds % SECONDS_A_WEEK === 0) {
return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK}
} else if (seconds % SECONDS_A_MONTH === 0) {
return {unit: 'days', amount: seconds / SECONDS_A_MONTH * 30}
} else if (seconds % SECONDS_A_YEAR === 0) {
return {unit: 'years', amount: seconds / SECONDS_A_YEAR}
} else {
return {unit: 'days', amount: seconds / SECONDS_A_DAY}
}

View File

@ -6,7 +6,11 @@ export function findById<T extends {id: string | number}>(array : T[], id : stri
return array.find(({id: currentId}) => currentId === id)
}
export function includesById(array: any[], id: string | number) {
interface ObjectWithId {
id: number
}
export function includesById(array: ObjectWithId[], id: string | number) {
return array.some(({id: currentId}) => currentId === id)
}

View File

@ -18,6 +18,8 @@ export const SUPPORTED_LOCALES = {
'es-ES': 'Español',
'da-DK': 'Dansk',
'ja-JP': '日本語',
'hu-HU': 'Magyar',
'ar-SA': 'اَلْعَرَبِيَّةُ',
} as const
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
@ -32,6 +34,7 @@ export const i18n = createI18n({
legacy: false,
messages: {
[DEFAULT_LANGUAGE]: langEN,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as Record<SupportedLocale, any>,
})
@ -47,8 +50,13 @@ export async function setLanguage(lang: SupportedLocale): Promise<SupportedLocal
// If the language hasn't been loaded yet
if (!i18n.global.availableLocales.includes(lang)) {
const messages = await import(`./lang/${lang}.json`)
i18n.global.setLocaleMessage(lang, messages.default)
try {
const messages = await import(`./lang/${lang}.json`)
i18n.global.setLocaleMessage(lang, messages.default)
} catch (e) {
console.error(`Failed to load language ${lang}:`, e)
return setLanguage(getBrowserLanguage())
}
}
i18n.global.locale.value = lang

1105
src/i18n/lang/ar-SA.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,11 +6,17 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -139,12 +145,39 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
"title": "Delete your Vikunja Account",
"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": "To proceed, please enter your password. You will receive an email with further instructions.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "Delete my account",
"requestSuccess": "The request was successful. You'll receive an email with further instructions.",
"passwordRequired": "Please enter your password.",
@ -152,6 +185,7 @@
"scheduled": "We will delete your Vikunja account at {date} ({dateSince}).",
"scheduledCancel": "To cancel the deletion of your account, click here.",
"scheduledCancelText": "To cancel the deletion of your account, please enter your password below:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Cancel the deletion of my account",
"scheduledCancelSuccess": "We will not delete your account."
},
@ -225,7 +259,7 @@
"identifier": "Project Identifier",
"identifierPlaceholder": "The project identifier goes here…",
"description": "Description",
"descriptionPlaceholder": "The projects description goes here…",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "Color",
"success": "The project was successfully updated."
},
@ -305,6 +339,9 @@
"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",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favorites"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
"description": "Description",
"descriptionPlaceholder": "The description goes here…",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"includeNulls": "Include Tasks which don't have a value set",
"requireAll": "Require all filters to be true for a task to show up",
"showDoneTasks": "Show Done Tasks",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",
@ -444,6 +498,7 @@
"custom": "Custom",
"id": "ID",
"created": "Created at",
"createdBy": "Created by {0}",
"actions": "Actions",
"cannotBeUndone": "This cannot be undone!"
},
@ -462,24 +517,59 @@
"edit": "Edit",
"done": "Done",
"heading1": "Heading 1",
"heading1Tooltip": "Big section heading.",
"heading2": "Heading 2",
"heading2Tooltip": "Medium section heading.",
"heading3": "Heading 3",
"heading3Tooltip": "Smaller section header.",
"headingSmaller": "Heading Smaller",
"headingBigger": "Heading Bigger",
"bold": "Bold",
"italic": "Italic",
"strikethrough": "Strikethrough",
"underline": "Underline",
"code": "Code",
"codeTooltip": "Capture a code snippet.",
"quote": "Quote",
"unorderedList": "Unordered List",
"orderedList": "Ordered List",
"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": "Clean Block",
"link": "Link",
"image": "Image",
"table": "Table",
"imageTooltip": "Upload an image from your computer.",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
},
"horizontalRule": "Horizontal Rule",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "Side By Side",
"guide": "Guide"
"guide": "Guide",
"text": "Text",
"textTooltip": "Just start typing with plain text.",
"taskList": "Task list",
"taskListTooltip": "Track tasks with a to-do list.",
"undo": "Undo",
"redo": "Redo",
"placeholder": "Type some text or hit '/' to see more options…"
},
"multiselect": {
"createPlaceholder": "Create new",
@ -573,6 +663,9 @@
"belongsToProject": "This task belongs to project '{project}'",
"due": "Due {at}",
"closePopup": "Close popup",
"organization": "Organization",
"management": "Management",
"dateAndTime": "Date and time",
"delete": {
"header": "Delete this task",
"text1": "Are you sure you want to remove this task?",
@ -650,7 +743,7 @@
"loading": "Loading comments…",
"edited": "edited {date}",
"creating": "Creating comment…",
"placeholder": "Add your comment…",
"placeholder": "Add your comment, hit '/' for more options…",
"comment": "Comment",
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
@ -664,7 +757,7 @@
"1week": "1 week"
},
"description": {
"placeholder": "Click here to enter a description…",
"placeholder": "Enter a description, hit '/' for more options…",
"empty": "No description available yet."
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "Every Day",
"everyWeek": "Every Week",
"everyMonth": "Every Month",
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From Current Date",
@ -815,7 +908,7 @@
"namePlaceholder": "The team's name goes here…",
"nameRequired": "Please specify a name.",
"description": "Description",
"descriptionPlaceholder": "The teams description goes here…",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
}
@ -843,7 +936,8 @@
"description": "Toggle editing of the task description",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
"favorite": "Mark this task as favorite / unfavorite",
"save": "Save the current task"
},
"project": {
"title": "Project Views",
@ -881,7 +975,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different 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": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -893,7 +987,9 @@
"notification": {
"title": "Notifications",
"none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
"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": "Commands",
@ -902,6 +998,7 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -5,12 +5,18 @@
"welcomeDay": "Ahoj {username}!",
"welcomeEvening": "Dobrý večer {username}!",
"lastViewed": "Naposledy zobrazeno",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"addToHomeScreen": "Přidejte tuto aplikaci na domovskou obrazovku pro rychlejší přístup a lepší zážitek.",
"goToOverview": "Přejít na přehled",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
"importText": "Importujte své projekty a úkoly z jiných služeb do Vikunja:",
"import": "Importujte svá data do Vikunja"
}
},
"demo": {
"title": "Tato instance je v ukázkovém režimu. Nepoužívejte ji pro skutečná data!",
"everythingWillBeDeleted": "Všechno bude v pravidelných intervalech smazáno!",
"accountWillBeDeleted": "Váš účet bude smazán včetně všech projektů, úkolů a příloh, které můžete vytvořit."
},
"404": {
"title": "Nenalezeno",
"text": "Požadovaná stránka neexistuje."
@ -77,17 +83,17 @@
"savedSuccess": "Nastavení bylo úspěšně aktualizováno.",
"emailReminders": "Posílat mi připomenutí pro úkoly e-mailem",
"overdueReminders": "Pošlete mi každý den shrnutí mých zpožděných úkolů",
"discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
"discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
"discoverableByName": "Umožnit ostatním uživatelům přidat mě jako člena do týmů nebo projektů, když hledají mé jméno",
"discoverableByEmail": "Umožnit ostatním uživatelům, aby mě přidali jako člena do týmů nebo projektů, když hledají můj úplný e-mail",
"playSoundWhenDone": "Přehrát zvuk při označení úkolů jako hotovo",
"weekStart": "Začátek týdne",
"weekStart": "První den týdne",
"weekStartSunday": "Neděle",
"weekStartMonday": "Pondělí",
"language": "Jazyk",
"defaultProject": "Default Project",
"defaultProject": "Výchozí projekt",
"timezone": "Časové pásmo",
"overdueTasksRemindersTime": "Čas odeslání emailu o zpožděných úkolech",
"filterUsedOnOverview": "Saved filter used on the overview page"
"filterUsedOnOverview": "Uložený filtr použitý na stránce přehledu"
},
"totp": {
"title": "Dvoufaktorové ověření",
@ -139,12 +145,39 @@
"system": "Systém",
"dark": "Tmavý"
}
},
"apiTokens": {
"title": "API Tokeny",
"general": "API tokeny umožňují používat Vikunja API bez uživatelských údajů.",
"apiDocs": "Podívejte se na dokumentaci api",
"createAToken": "Vytvořit token",
"createToken": "Vytvořit token",
"30d": "30 dní",
"60d": "60 dní",
"90d": "90 dní",
"permissionExplanation": "Oprávnění vám umožní nastavit, k čemu lze api token použít.",
"titleRequired": "Je vyžadován název",
"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íš!",
"delete": {
"header": "Smazat tento token",
"text1": "Opravdu chcete smazat token \"{token}\"?",
"text2": "Tímto zrušíte přístup ke všem aplikacím nebo integracím, které ho používají. Toto nelze vrátit zpět."
},
"attributes": {
"title": "Název",
"titlePlaceholder": "Zadejte název, podle kterýho ho později poznáte",
"expiresAt": "Vyprší v",
"permissions": "Oprávnění"
}
}
},
"deletion": {
"title": "Smazat svůj účet",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
"text1": "Odstranění vašeho účtu je trvalé a nelze ho vrátit zpět. Vymažeme všechny vaše projekty, úkoly a vše, co je s ním spojeno.",
"text2": "Chcete-li pokračovat, zadejte své heslo. Obdržíte e-mail s dalšími pokyny.",
"text3": "Chcete-li pokračovat, klikněte na tlačítko níže. Obdržíte e-mail s dalšími pokyny.",
"confirm": "Smazat můj účet",
"requestSuccess": "Požadavek byl úspěšný. Obdržíte e-mail s dalšími pokyny.",
"passwordRequired": "Prosím zadejte Vaše heslo.",
@ -152,12 +185,13 @@
"scheduled": "Váš Vikunja účet odstraníme do {date} ({dateSince}).",
"scheduledCancel": "Chcete-li zrušit smazání vašeho účtu, klikněte zde.",
"scheduledCancelText": "Chcete-li zrušit smazání vašeho účtu, zadejte prosím své heslo níže:",
"scheduledCancelButton": "Chcete-li zrušit odstranění vašeho účtu, klikněte na tlačítko níže:",
"scheduledCancelConfirm": "Zrušit smazání mého účtu",
"scheduledCancelSuccess": "Váš účet nebude smazán."
},
"export": {
"title": "Exportovat data účtu",
"description": "You can request a copy of all your Vikunja data. This includes Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"description": "Můžete si vyžádat kopii všech vašich dat Vikunje. To zahrnuje projekty, úkoly a vše, co je s nimi spojeno. Tato data můžete importovat v libovolné instanci Vikunja prostřednictvím migrační funkce.",
"descriptionPasswordRequired": "Pokračujte zadáním vašeho hesla:",
"request": "Požádat o kopii mých dat",
"success": "Úspěšně jste požádali o svá data! Jakmile budou připravena ke stažení, pošleme Vám e-mail.",
@ -165,164 +199,182 @@
}
},
"project": {
"archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archived",
"showArchived": "Show Archived",
"title": "Project Title",
"color": "Color",
"projects": "Projects",
"parent": "Parent Project",
"search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this project",
"shared": "Shared Projects",
"noDescriptionAvailable": "No project description is available.",
"archivedMessage": "Tento projekt je archivován. Není možné vytvořit ani upravovat jeho úkoly.",
"archived": "Archivováno",
"showArchived": "Zobrazit archivované",
"title": "Název projektu",
"color": "Barva",
"projects": "Projekty",
"parent": "Nadřazený projekt",
"search": "Začni psát pro vyhledání projektu…",
"searchSelect": "Klikněte nebo stiskněte Enter pro výběr tohoto projektu",
"shared": "Sdílené projekty",
"noDescriptionAvailable": "Není k dispozici žádný popis projektu.",
"inboxTitle": "Inbox",
"create": {
"header": "New project",
"titlePlaceholder": "The project's title goes here…",
"addTitleRequired": "Please specify a title.",
"createdSuccess": "The project was successfully created.",
"addProjectRequired": "Please specify a project or set a default project in the settings."
"header": "Nový projekt",
"titlePlaceholder": "Název projektu přijde sem…",
"addTitleRequired": "Zadejte název prosím.",
"createdSuccess": "Projekt byl úspěšně vytvořen.",
"addProjectRequired": "Vyberte projekt nebo nastavte výchozí projekt v nastavení."
},
"archive": {
"title": "Archive \"{project}\"",
"archive": "Archive this project",
"unarchive": "Un-Archive this project",
"unarchiveText": "You will be able to create new tasks or edit it.",
"archiveText": "You won't be able to edit this project or create new tasks until you un-archive it.",
"success": "The project was successfully archived."
"title": "Archivovat \"{project}\"",
"archive": "Archivovat tento projekt",
"unarchive": "Zrušit archivaci tohoto projektu",
"unarchiveText": "Budete moci vytvářet nové úkoly nebo je upravovat.",
"archiveText": "Tento projekt nebudete moci upravovat ani v něm vytvářet nové úkoly, dokud jej neodarchivujete.",
"success": "Projekt byl úspěšně archivován."
},
"background": {
"title": "Set project background",
"remove": "Remove Background",
"upload": "Choose a background from your pc",
"searchPlaceholder": "Search for a background…",
"poweredByUnsplash": "Powered by Unsplash",
"loadMore": "Load more photos",
"success": "The background has been set successfully!",
"removeSuccess": "The background has been removed successfully!"
"title": "Nastavit pozadí projektu",
"remove": "Odstranit pozadí",
"upload": "Vyberte si pozadí ze svého počítače",
"searchPlaceholder": "Hledat pozadí…",
"poweredByUnsplash": "Běží na Unsplash",
"loadMore": "Načíst více fotek",
"success": "Pozadí bylo úspěšně nastaveno!",
"removeSuccess": "Pozadí bylo úspěšně odebráno!"
},
"delete": {
"title": "Delete \"{project}\"",
"header": "Delete this project",
"text1": "Are you sure you want to delete this project and all of its contents?",
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
"title": "Smazat \"{project}\"",
"header": "Odstranit tento projekt",
"text1": "Opravdu chcete smazat projekt a všechny jeho úkoly?",
"text2": "To zahrnuje všechny úkoly a JE TO NEVRATNÉ!",
"success": "Projekt byl úspěšně smazán.",
"tasksToDelete": "Neodvolatelně tím odstraníme asi {count} úloh.",
"noTasksToDelete": "Tento projekt neobsahuje žádné úkoly, mělo by být bezpečné ho smazat."
},
"duplicate": {
"title": "Duplicate this project",
"label": "Duplicate",
"text": "Select a parent project which should hold the duplicated project:",
"success": "The project was successfully duplicated."
"title": "Duplikovat projekt",
"label": "Duplikovat",
"text": "Vyberte projekt nadřazený duplikovanému projektu:",
"success": "Projekt byl úspěšně duplikován."
},
"edit": {
"header": "Edit This Project",
"title": "Edit \"{project}\"",
"titlePlaceholder": "The project title goes here…",
"identifierTooltip": "The project identifier can be used to uniquely identify a task across projects. You can set it to empty to disable it.",
"identifier": "Project Identifier",
"identifierPlaceholder": "The project identifier goes here…",
"description": "Description",
"descriptionPlaceholder": "The projects description goes here…",
"color": "Color",
"success": "The project was successfully updated."
"header": "Upravit tento projekt",
"title": "Upravit \"{project}\"",
"titlePlaceholder": "Název projektu přijde sem…",
"identifierTooltip": "Identifikátor projektu může být použit k jedinečné identifikaci úkolu napříč projekty. Můžete jej nastavit jako prázdný pro jeho vypnutí.",
"identifier": "Identifikátor projektu",
"identifierPlaceholder": "Identifikátor projektu přijde sem…",
"description": "Popis",
"descriptionPlaceholder": "Zadejte popis tohoto projektu, stiskněte '/' pro další možnosti…",
"color": "Barva",
"success": "Projekt byl úspěšně aktualizován."
},
"share": {
"header": "Share this project",
"title": "Share \"{project}\"",
"share": "Share",
"header": "Sdílet tento projekt",
"title": "Sdílet \"{project}\"",
"share": "Sdílet",
"links": {
"title": "Share Links",
"what": "What is a share link?",
"explanation": "Share Links allow you to easily share a project with other users who don't have an account on Vikunja.",
"create": "Create a new link share",
"name": "Name (optional)",
"namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.",
"password": "Password (optional)",
"passwordExplanation": "When signing in, the user will be required to enter this password.",
"noName": "No name set",
"remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
"createSuccess": "The link share was successfully created.",
"deleteSuccess": "The link share was successfully deleted",
"view": "View",
"sharedBy": "Shared by {0}"
"title": "Sdílené odkazy",
"what": "Co je to sdílený odkaz?",
"explanation": "Sdílené odkazy vám umožní snadno sdílet projekt s ostatními uživateli, kteří nemají účet na Vikunja.",
"create": "Vytvořit sdílený odkaz",
"name": "Název (volitelné)",
"namePlaceholder": "např. Lorem Ipsum",
"nameExplanation": "Všechny akce provedené tímto sdíleným odkazem se zobrazí s tímto názvem.",
"password": "Heslo (volitelné)",
"passwordExplanation": "Při přihlášení bude uživatel muset zadat toto heslo.",
"noName": "Není nastaven žádný název",
"remove": "Odstranit sdílený odkaz",
"removeText": "Jste si jisti, že chcete odstranit tento sdílený odkaz? K tomuto projektu již nebude možné přistupovat s tímto sdíleným odkazem. Tuto akci nelze vrátit zpět!",
"createSuccess": "Sdílený odkaz byl úspěšně vytvořen.",
"deleteSuccess": "Sdílený odkaz byl úspěšně smazán",
"view": "Zobrazit",
"sharedBy": "Sdílel(a) {0}"
},
"userTeam": {
"typeUser": "user | users",
"typeTeam": "team | teams",
"shared": "Shared with these {type}",
"you": "You",
"notShared": "Not shared with any {type} yet.",
"removeHeader": "Remove a {type} from the {sharable}",
"removeText": "Are you sure you want to remove this {sharable} from the {type}? This cannot be undone!",
"removeSuccess": "The {sharable} was successfully removed from the {type}.",
"addedSuccess": "The {type} was successfully added.",
"updatedSuccess": "The {type} was successfully added."
"typeUser": "uživatel | uživatelé",
"typeTeam": "tým | týmy",
"shared": "Sdíleno s těmito {type}",
"you": "Ty",
"notShared": "Zatím není sdíleno s žádným {type}.",
"removeHeader": "Odstranit {type} z {sharable}",
"removeText": "Jste si jisti, že chcete odstranit {sharable} z {type}.? Tuto akci nelze vrátit zpět!",
"removeSuccess": "{sharable} byl úspěšně odebrán z {type}.",
"addedSuccess": "{type} byl úspěšně přidán.",
"updatedSuccess": "{type} byl úspěšně přidán."
},
"right": {
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
"title": "Oprávnění",
"read": "Pouze pro čtení",
"readWrite": "Čtení a zápis",
"admin": "Administrátor"
},
"attributes": {
"link": "Link",
"delete": "Delete"
"link": "Odkaz",
"delete": "Smazat"
}
},
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This project is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
"title": "Seznam",
"add": "Přidat",
"addPlaceholder": "Přidat nový úkol…",
"empty": "Tento projekt je zatím prázdný.",
"newTaskCta": "Vytvořit nový úkol.",
"editTask": "Upravit úkol"
},
"gantt": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
"size": "Size",
"default": "Default",
"month": "Month",
"day": "Day",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
"showTasksWithoutDates": "Zobrazit úkoly, které nemají nastavené datum",
"size": "Velikost",
"default": "Výchozí",
"month": "Měsíc",
"day": "Den",
"hour": "Hodina",
"range": "Časové období",
"noDates": "Tento úkol nemá nastaveno žádné datum."
},
"table": {
"title": "Table",
"columns": "Columns"
"title": "Tabulka",
"columns": "Sloupce"
},
"kanban": {
"title": "Kanban",
"limit": "Limit: {limit}",
"noLimit": "Not Set",
"doneBucket": "Done bucket",
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"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"
"noLimit": "Nenastaveno",
"doneBucket": "Sloupec \"Hotovo\"",
"doneBucketHint": "Všechny úkoly přesunuté do tohoto sloupce budou automaticky označeny jako dokončené.",
"doneBucketHintExtended": "Všechny úkoly přesunuté do sloupce \"Hotovo\" budou označeny jako dokončené automaticky. Všechny úkoly označené jako dokončené jinde sem budou přesunuty také.",
"doneBucketSavedSuccess": "Sloupec \"Hotovo\" byl úspěšně uložen.",
"defaultBucket": "Výchozí sloupec",
"defaultBucketHint": "Při vytváření úkolů bez uvedení sloupce, budou přidány do tohoto sloupce.",
"defaultBucketSavedSuccess": "Výchozí sloupec byl úspěšně uložen.",
"deleteLast": "Nelze odstranit poslední sloupec.",
"addTaskPlaceholder": "Zadejte nový název úkolu…",
"addTask": "Přidat úkol",
"addAnotherTask": "Přidat další úkol",
"addBucket": "Vytvořit nový sloupec",
"addBucketPlaceholder": "Zadejte název nového sloupce…",
"deleteHeaderBucket": "Smazat sloupec",
"deleteBucketText1": "Opravdu chcete smazat tento sloupec?",
"deleteBucketText2": "Toto nesmaže žádné úkoly, ale přesune je do výchozího sloupce.",
"deleteBucketSuccess": "Sloupec byl úspěšně smazán.",
"bucketTitleSavedSuccess": "Název sloupce byl úspěšně uložen.",
"bucketLimitSavedSuccess": "Limit sloupce byl úspěšně uložen.",
"collapse": "Sbalit tento sloupec"
},
"pseudo": {
"favorites": {
"title": "Favorites"
"title": "Oblíbené"
}
},
"webhooks": {
"title": "Webhooky",
"targetUrl": "Cílová URL",
"targetUrlInvalid": "Zadejte prosím platnou URL adresu.",
"events": "Události",
"eventsHint": "Vyberte všechny události, pro které by tento webhook měl dostávat aktualizace (v rámci aktuálního projektu).",
"mustSelectEvents": "Musíte zvolit alespoň jednu událost.",
"delete": "Smazat tento webhook",
"deleteText": "Opravdu chcete odstranit tento webhook? Externí cíle již nebudou informovány o jeho událostech.",
"deleteSuccess": "Webhook byl úspěšně odstraněn.",
"create": "Vytvořit webhook",
"secret": "Tajný klíč",
"secretHint": "Pokud je zadáno, všechny požadavky na cílovou adresu URL webhooku budou podepsány pomocí HMAC.",
"secretDocs": "Další podrobnosti o používání tajných klíčů naleznete v dokumentaci."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Název",
"titlePlaceholder": "Název uloženého filtru přijde sem…",
"description": "Popis",
"descriptionPlaceholder": "Popis přijde sem…",
"descriptionPlaceholder": "Přidejte popis pro tento filtr, stiskněte '/' pro více možností…",
"includeNulls": "Zahrnout úkoly, které nemají nastavenou hodnotu",
"requireAll": "Vyžaduje aby všechny filtry odpovídaly, aby se úkol zobrazil",
"showDoneTasks": "Zobrazit dokončené úkoly",
@ -346,9 +398,9 @@
},
"create": {
"title": "Nový uložený filtr",
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
"description": "Uložený filtr je virtuální projekt, který se počítá ze sady filtrů pokaždé, když je k němu přistupováno.",
"action": "Vytvořit uložený filtr",
"titleRequired": "Please provide a title for the filter."
"titleRequired": "Uveďte název filtru."
},
"delete": {
"header": "Smazat tento uložený filtr",
@ -361,7 +413,7 @@
}
},
"migrate": {
"title": "Import from other services",
"title": "Import z jiných služeb",
"titleService": "Importujte svá data z {name} do Vikunja",
"import": "Importujte svá data do Vikunja",
"description": "Chcete-li začít, klikněte na logo jedné ze služeb třetích stran.",
@ -373,12 +425,14 @@
"alreadyMigrated2": "Import je možný, ale mohl by vytvářet duplicity. Jste si jisti?",
"confirm": "Jsem si jistý, začněte migrovat!",
"importUpload": "Chcete-li importovat data z {name} do Vikunja, klikněte na tlačítko níže pro výběr souboru.",
"upload": "Nahrát soubor"
"upload": "Nahrát soubor",
"migrationStartedWillReciveEmail": "Vikunja nyní importuje vaše seznamy/projekty, úkoly, poznámky, připomenutí a soubory z {service}. Protože to bude chvíli trvat, pošleme vám e-mail až bude hotovo. Toto okno můžete nyní zavřít.",
"migrationInProgress": "Probíhá migrace. Počkejte prosím na její dokončení."
},
"label": {
"title": "Štítky",
"manage": "Spravovat štítky",
"description": "Click on a label to edit it. You can edit all labels you created, you can use all labels which are associated with a task to whose project you have access.",
"description": "Klikněte na štítek pro jeho úpravu. Můžete upravit všechny štítky, které jste vytvořili, můžete použít všechny štítky, které jsou přiřazeny k úkolu z projektu, do kterého máte přístup.",
"newCTA": "Momentálně nemáte žádné štítky.",
"search": "Zadejte hledaný štítek…",
"create": {
@ -389,7 +443,7 @@
},
"edit": {
"header": "Upravit štítek",
"forbidden": "You are not allowed to edit this label because you don't own it.",
"forbidden": "Nemáte oprávnění upravovat tento popisek, protože ho nevlastníte.",
"success": "Štítek byl úspěšně aktualizován."
},
"deleteSuccess": "Štítek byl úspěšně smazán.",
@ -403,7 +457,7 @@
},
"sharing": {
"authenticating": "Ověřování…",
"passwordRequired": "This shared project requires a password. Please enter it below:",
"passwordRequired": "Tento sdílený projekt vyžaduje heslo. Zadejte jej níže:",
"error": "Došlo k chybě.",
"invalidPassword": "Neplatné heslo."
},
@ -444,6 +498,7 @@
"custom": "Vlastní",
"id": "ID",
"created": "Vytvořeno",
"createdBy": "Vytvořil(a) {0}",
"actions": "Akce",
"cannotBeUndone": "Toto nelze vrátit!"
},
@ -462,24 +517,59 @@
"edit": "Upravit",
"done": "Hotovo",
"heading1": "Nadpis 1",
"heading1Tooltip": "Velké záhlaví.",
"heading2": "Nadpis 2",
"heading2Tooltip": "Střední záhlaví.",
"heading3": "Nadpis 3",
"heading3Tooltip": "Menší záhlaví.",
"headingSmaller": "Menší nadpis",
"headingBigger": "Větší nadpis",
"bold": "Tučné",
"italic": "Skloněné",
"strikethrough": "Přeškrtnuté",
"underline": "Podtržení",
"code": "Kód",
"codeTooltip": "Zachytit úryvek kódu.",
"quote": "Citace",
"unorderedList": "Seznam s odrážkami",
"orderedList": "Ordered List",
"quoteTooltip": "Zachyťte citaci.",
"bulletList": "Seznam s odrážkami",
"bulletListTooltip": "Vytvořit jednoduchý seznam odrážek.",
"unorderedList": "Nečíslovaný seznam",
"orderedList": "Číslovaný seznam",
"orderedListTooltip": "Vytvořit seznam s číslováním.",
"cleanBlock": "Čistý blok",
"link": "Odkaz",
"image": "Obrázek",
"table": "Tabulka",
"imageTooltip": "Nahrát obrázek z vašeho počítače.",
"table": {
"title": "Tabulka",
"insert": "Vložit tabulku",
"addColumnBefore": "Přidat sloupec před",
"addColumnAfter": "Přidat sloupec za",
"deleteColumn": "Smazat sloupec",
"addRowBefore": "Přidat řádek před",
"addRowAfter": "Přidat řádek za",
"deleteRow": "Smazat řádek",
"deleteTable": "Smazat tabulku",
"mergeCells": "Sloučit buňky",
"splitCell": "Rozdělit buňku",
"toggleHeaderColumn": "Přepnout sloupec záhlaví",
"toggleHeaderRow": "Přepnout řádek záhlaví",
"toggleHeaderCell": "Přepnout hlavičku",
"mergeOrSplit": "Sloučit nebo rozdělit",
"fixTables": "Opravit tabulky"
},
"horizontalRule": "Vodorovná čára",
"horizontalRuleTooltip": "Rozdělit sekci.",
"sideBySide": "Vedle sebe",
"guide": "Průvodce"
"guide": "Průvodce",
"text": "Text",
"textTooltip": "Stačí začít psát prostý text.",
"taskList": "Seznam úkolů",
"taskListTooltip": "Sledovat úkoly se seznamem úkolů.",
"undo": "Vrátit zpět",
"redo": "Opakovat akci",
"placeholder": "Zadejte nějaký text nebo stiskněte '/' pro zobrazení více možností…"
},
"multiselect": {
"createPlaceholder": "Vytvořit nový",
@ -509,14 +599,14 @@
"canuse": "Můžete použít vzorec pro filtrování podle relativních datumů.",
"learnhow": "Podívejte se, jak to funguje",
"title": "Datumový vzorec",
"intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
"intro": "Použijte relativní časové údaje, které budou vyřešeny za běhu po aplikování filtru.",
"expression": "Každý datumový matematický výraz začíná datem ukotvení, které může být buď {0}, nebo datový řetězec končící {1}. Po tomto ukotvení může volitelně následovat jeden nebo více matematických výrazů.",
"similar": "Tyto výrazy jsou podobné výrazům poskytnutým {0} a {1}.",
"add1Day": "Přidat jeden den",
"minus1Day": "Odečíst jeden den",
"roundDay": "Zaokrouhlit dolů na nejbližší den",
"supportedUnits": "Supported time units",
"someExamples": "Examples of time expressions",
"supportedUnits": "Podporované časové jednotky",
"someExamples": "Příklady časových výrazů",
"units": {
"seconds": "Sekundy",
"minutes": "Minuty",
@ -545,7 +635,7 @@
"addReminder": "Přidat novou připomínku…",
"doneSuccess": "Úkol byl úspěšně označen jako dokončený.",
"undoneSuccess": "Úkol byl úspěšně znovu otevřen.",
"undo": "Undo",
"undo": "Vrátit zpět",
"openDetail": "Otevřít zobrazení detailu úkolu",
"checklistTotal": "{checked} z {total} úkolů",
"checklistAllDone": "{total} úkolů",
@ -562,7 +652,7 @@
"chooseDueDate": "Klikněte zde pro nastavení termínu dokončení",
"chooseStartDate": "Klikněte zde pro nastavení počátečního data",
"chooseEndDate": "Klikněte zde pro nastavení data ukončení",
"move": "Move task to a different project",
"move": "Přesunout úkol do jiného projektu",
"done": "Označit úkol jako hotový!",
"undone": "Označit jako znovu otevřené",
"created": "Vytvořeno {0} uživatelem {1}",
@ -570,9 +660,12 @@
"doneAt": "Dokončeno {0}",
"updateSuccess": "Úkol byl úspěšně uložen.",
"deleteSuccess": "Úkol byl úspěšně smazán.",
"belongsToProject": "This task belongs to project '{project}'",
"belongsToProject": "Tento úkol patří do projektu '{project}'",
"due": "Termín {at}",
"closePopup": "Zavřít vyskakovací okno",
"organization": "Organizace",
"management": "Management",
"dateAndTime": "Datum a čas",
"delete": {
"header": "Smazat tento úkol",
"text1": "Opravdu chcete odstranit tento úkol?",
@ -590,7 +683,7 @@
"percentDone": "Nastavit průběh",
"attachments": "Přidat přílohy",
"relatedTasks": "Přidat vztah",
"moveProject": "Move",
"moveProject": "Přesunout",
"color": "Nastavit barvu",
"delete": "Smazat",
"favorite": "Přidat do oblíbených",
@ -617,15 +710,15 @@
"updated": "Aktualizováno"
},
"subscription": {
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTaskThroughParentProject": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto úkolu prostřednictvím jeho projektu.",
"subscribedProject": "Nyní jste přihlášeni k odběru tohoto projektu a budete dostávat oznámení o změnách.",
"notSubscribedProject": "Nejste přihlášeni k tomuto projektu a nebudete dostávat oznámení o změnách.",
"subscribedTask": "Nyní jste přihlášeni k odběru tohoto úkolu a budete dostávat oznámení o změnách.",
"notSubscribedTask": "Nejste přihlášeni k odběru tohoto úkolu, takže nebudete dostávat upozornění na změny.",
"subscribe": "Odebírat",
"unsubscribe": "Odhlásit odběr",
"subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessProject": "Nyní jste přihlášeni k odběru tohoto projektu",
"unsubscribeSuccessProject": "Nyní jste odhlášeni od odběru tohoto projektu",
"subscribeSuccessTask": "Nyní jste přihlášeni k tomuto úkolu",
"unsubscribeSuccessTask": "Nyní jste odhlášeni od tohoto úkolu"
},
@ -650,7 +743,7 @@
"loading": "Načítám komentáře…",
"edited": "upraveno {date}",
"creating": "Vytvářím komentář…",
"placeholder": "Přidejte svůj komentář…",
"placeholder": "Přidejte svůj komentář, stiskněte '/' pro více možností…",
"comment": "Komentář",
"delete": "Smazat tento komentář",
"deleteText1": "Opravdu chcete smazat tento komentář?",
@ -664,7 +757,7 @@
"1week": "1 týden"
},
"description": {
"placeholder": "Kliknutím sem zadejte popis…",
"placeholder": "Zadejte popis, stiskněte '/' pro více možností…",
"empty": "Ještě není k dispozici žádný popis."
},
"assignee": {
@ -699,7 +792,7 @@
"new": "Nový vztah k úkolu",
"searchPlaceholder": "Hledejte nový úkol, který chcete přidat jako související…",
"createPlaceholder": "Přidat toto jako nový související úkol",
"differentProject": "This task belongs to a different project.",
"differentProject": "Tento úkol patří do jiného projektu.",
"noneYet": "Zatím žádné vztahy mezi úkoly.",
"delete": "Odstranit vztah k úloze",
"deleteText1": "Jste si jisti, že chcete odstranit tento vztah úkolu?",
@ -720,20 +813,20 @@
}
},
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
"before": "{amount} {unit} před {type}",
"after": "{amount} {unit} po {type}",
"beforeShort": "před",
"afterShort": "po",
"onDueDate": "V termínu",
"onStartDate": "V den zahájení",
"onEndDate": "V den ukončení",
"custom": "Vlastní",
"dateAndTime": "Datum a čas"
},
"repeat": {
"everyDay": "Každý den",
"everyWeek": "Každý týden",
"everyMonth": "Každý měsíc",
"every30d": "Každých 30 dní",
"mode": "Režim opakování",
"monthly": "Měsíčně",
"fromCurrentDate": "Od aktuálního data",
@ -747,7 +840,7 @@
"invalidAmount": "Zadejte prosím více než 0."
},
"quickAddMagic": {
"hint": "Use magic prefixes to define due dates, assignees and other task properties.",
"hint": "Používejte magické prefixy pro definování termínů, přiřazených osob a dalších vlastností úkolů.",
"title": "Kouzelné rychlé přidání",
"intro": "Při vytváření úkolu můžete použít speciální klíčová slova pro přímé přidání atributů k nově vytvořenému úkolu. To umožňuje přidat běžně používané atributy k úkolům mnohem rychleji.",
"multiple": "Toto můžete použít několikrát.",
@ -758,10 +851,10 @@
"priority1": "Chcete-li nastavit prioritu úkolu, přidejte číslo 1-5, s prefixem {prefix}.",
"priority2": "Čím vyšší číslo, tím vyšší priorita.",
"assignees": "Chcete-li přímo přiřadit úkol k uživateli, přidejte k úkolu jejich uživatelské jméno s prefixem {prefix}.",
"project1": "To set a project for the task to appear in, enter its name prefixed with {prefix}.",
"project2": "This will return an error if the project does not exist.",
"project3": "To use spaces, simply add a \" or ' around the project name.",
"project4": "For example: {prefix}\"Project with spaces\".",
"project1": "Chcete-li nastavit projekt pro zobrazený úkol, zadejte jeho název s předponou {prefix}.",
"project2": "Toto vrátí chybu, pokud projekt neexistuje.",
"project3": "Chcete-li použít mezery, stačí přidat \" nebo ' kolem názvu projektu.",
"project4": "Například: {prefix}\"Projekt s mezerami\".",
"dateAndTime": "Datum a čas",
"date": "Jakékoliv datum bude použito jako datum dokončení nového úkolu. Můžete použít data v kterémkoli z těchto formátů:",
"dateWeekday": "každý pracovní den použije další datum s tímto datem",
@ -794,19 +887,19 @@
"delete": {
"header": "Smazat tým",
"text1": "Jste si jisti, že chcete smazat tento tým a všechny jeho členy?",
"text2": "All team members will lose access to projects shared with this team. This CANNOT BE UNDONE!",
"text2": "Všichni členové týmu ztratí přístup k projektům sdíleným s tímto týmem. NELZE TO VZÍT ZPĚT!",
"success": "Tým byl úspěšně smazán."
},
"deleteUser": {
"header": "Odebrat uživatele z týmu",
"text1": "Opravdu chcete odebrat tohoto uživatele z týmu?",
"text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
"text2": "Ztratí přístup ke všem projektům, k nimž má tento tým přístup. NELZE VZÍT ZPĚT!",
"success": "Uživatel byl úspěšně odstraněn z týmu."
},
"leave": {
"title": "Opustit tým",
"text1": "Opravdu chcete opustit tento tým?",
"text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "Ztratíte přístup ke všem projektům, k nimž má tento tým přístup. Pokud změníte názor, budete potřebovat správce týmu, aby vás znovu přidal.",
"success": "Úspěšně jste opustili tým."
}
},
@ -815,7 +908,7 @@
"namePlaceholder": "Název týmu přijde sem…",
"nameRequired": "Zadejte název.",
"description": "Popis",
"descriptionPlaceholder": "Popis týmu přijde sem…",
"descriptionPlaceholder": "Popište tým, stiskněte '/' pro více možností…",
"admin": "Administrátor",
"member": "Člen"
}
@ -838,19 +931,20 @@
"attachment": "Přidat přílohu k tomuto úkolu",
"related": "Upravit související úkoly tohoto úkolu",
"color": "Změnit barvu tohoto úkolu",
"move": "Move this task to another project",
"move": "Přesunout tento úkol do jiného projektu",
"reminder": "Spravovat připomenutí této úlohy",
"description": "Přepnout úpravy popisu úkolu",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
"delete": "Smazat tento úkol",
"priority": "Změnit prioritu tohoto úkolu",
"favorite": "Označit tuto úlohu jako oblíbenou / odebrat oblíbené",
"save": "Uložit aktuální úkol"
},
"project": {
"title": "Project Views",
"switchToListView": "Switch to list view",
"switchToGanttView": "Switch to gantt view",
"switchToKanbanView": "Switch to kanban view",
"switchToTableView": "Switch to table view"
"title": "Zobrazení projektu",
"switchToListView": "Přepnout na zobrazení seznamu",
"switchToGanttView": "Přepnout na zobrazení gantt",
"switchToKanbanView": "Přepnout na zobrazení kanbanu",
"switchToTableView": "Přepnout na zobrazení tabulky"
},
"navigation": {
"title": "Navigace",
@ -858,11 +952,11 @@
"upcoming": "Přejít na nadcházející úkoly",
"labels": "Přejít na štítky",
"teams": "Přejít na týmy",
"projects": "Navigate to projects"
"projects": "Přejít na projekty"
}
},
"update": {
"available": "There is an update available!",
"available": "K dispozici je aktualizace!",
"do": "Aktualizovat nyní"
},
"menu": {
@ -873,15 +967,15 @@
"unarchive": "Zrušit archivaci",
"setBackground": "Nastavit pozadí",
"share": "Sdílet",
"newProject": "New project",
"createProject": "Create project"
"newProject": "Nový projekt",
"createProject": "Vytvořit projekt"
},
"apiConfig": {
"url": "Vikunja URL",
"urlPlaceholder": "např. https://localhost:3456",
"change": "změnit",
"use": "Používá se instalace Vikunja v {0}",
"error": "Nelze najít nebo použít instalaci Vikunja na \"{domain}\". Zkuste prosím jinou url.",
"error": "Nelze najít nebo použít instalaci Vikunja na \"{domain}\". Zkontrolujte, zda má URL správný formát a můžete se k ní připojit při přímém přístupu a zkuste to znovu.",
"success": "Pomocí instalace Vikunja na \"{domain}\".",
"urlRequired": "Je vyžadována adresa URL."
},
@ -893,23 +987,26 @@
"notification": {
"title": "Oznámení",
"none": "Nemáte žádná oznámení. Mějte příjemný den!",
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
"explainer": "Upozornění se zobrazí zde, když proběhne akce na projektech nebo úkolech, ke kterým jste se přihlásili.",
"markAllRead": "Označit všechna oznámení za přečtená",
"markAllReadSuccess": "Všechna oznámení byla označena jako přečtená."
},
"quickActions": {
"commands": "Příkazy",
"placeholder": "Napište příkaz nebo vyhledávání…",
"hint": "You can use {project} to limit the search to a project. Combine {project} or {label} (labels) with a search query to search for a task with these labels or on that project. Use {assignee} to only search for teams.",
"hint": "Můžete použít {project} k omezení vyhledávání na projekt. Kombinujte {project} nebo {label} (štítky) s vyhledávacím dotazem pro hledání úkolu s těmito štítky nebo na tomto projektu. Použijte {assignee} pouze pro hledání týmů.",
"tasks": "Úkoly",
"projects": "Projects",
"projects": "Projekty",
"teams": "Týmy",
"newProject": "Enter the title of the new project…",
"labels": "Štítky",
"newProject": "Zadejte název nového projektu…",
"newTask": "Zadejte název nového úkolu…",
"newTeam": "Zadejte název nového týmu…",
"createTask": "Create a task in the current project ({title})",
"createProject": "Create a project",
"createTask": "Vytvořit úkol v aktuálním projektu ({title})",
"createProject": "Vytvořit projekt",
"cmds": {
"newTask": "Nový úkol",
"newProject": "New project",
"newProject": "Nový projekt",
"newTeam": "Nový tým"
}
},
@ -940,15 +1037,15 @@
"1018": "Nastavení typu avatara uživatele je neplatné.",
"2001": "ID nemůže být prázdné nebo 0.",
"2002": "Některé údaje požadavku byly neplatné.",
"3001": "The project does not exist.",
"3004": "You need to have read permissions on that project to perform that action.",
"3005": "The project title cannot be empty.",
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"3001": "Projekt neexistuje.",
"3004": "Pro provedení této akce musíte mít oprávnění ke čtení k tomuto projektu.",
"3005": "Název projektu nemůže být prázdný.",
"3006": "Sdílení projektu neexistuje.",
"3007": "Projekt s tímto identifikátorem již existuje.",
"3008": "Projekt je archivován, a proto je přístupný pouze pro čtení. To platí i pro všechny úkoly spojené s tímto projektem.",
"4001": "Text úkolu projektu nemůže být prázdný.",
"4002": "Úkol projektu neexistuje.",
"4003": "Všechny úkoly pro hromadnou úpravu musí patřit do stejného projektu.",
"4004": "Při hromadných úpravách úkolů je potřeba alespoň jeden úkol.",
"4005": "Nemáte právo vidět tento úkol.",
"4006": "Nadřazený úkol nelze nastavit jako takový.",
@ -967,21 +1064,21 @@
"4019": "Neplatná hodnota filtru úkolů.",
"6001": "Název týmu nemůže být prázdný.",
"6002": "Tým neexistuje.",
"6004": "The team already has access to that project.",
"6004": "Tým již má k tomuto projektu přístup.",
"6005": "Uživatel je již členem tohoto týmu.",
"6006": "Nelze odstranit posledního člena týmu.",
"6007": "The team does not have access to the project to perform that action.",
"7002": "The user already has access to that project.",
"7003": "You do not have access to that project.",
"6007": "Tým nemá přístup k seznamu pro provedení této akce.",
"7002": "Uživatel již má přístup k tomuto projektu.",
"7003": "K tomuto projektu nemáte přístup.",
"8001": "Tento štítek již v tomto úkolu existuje.",
"8002": "Štítek neexistuje.",
"8003": "K tomuto štítku nemáte přístup.",
"9001": "Právo je neplatné.",
"10001": "Sloupec neexistuje.",
"10002": "The bucket does not belong to that project.",
"10003": "You cannot remove the last bucket on a project.",
"10002": "Sloupec nepatří do tohoto projektu.",
"10003": "Poslední sloupec v projektu nelze odstranit.",
"10004": "Nemůžete přidat úkol do tohoto sloupce, protože již překročil limit úkolů, které do něj můžete uložit.",
"10005": "There can be only one done bucket per project.",
"10005": "V projektu může být pouze jeden sloupec \"Hotovo\".",
"11001": "Uložený filtr neexistuje.",
"11002": "Uložené filtry nejsou k dispozici pro sdílení odkazů.",
"12001": "Typ předplatného je neplatný.",
@ -996,13 +1093,13 @@
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
"seconds": "sekunda|sekund",
"minutes": "minuta|minut",
"hours": "hodina|hodin",
"days": "den|dny",
"weeks": "týden|týdny",
"months": "měsíc|měsíce",
"years": "rok|roky"
}
}
}

View File

@ -6,11 +6,17 @@
"welcomeEvening": "Godaften {username}!",
"lastViewed": "Sidst vist",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Ikke fundet",
"text": "Den ønskede side findes ikke."
@ -139,12 +145,39 @@
"system": "System",
"dark": "Mørk"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
"title": "Slet din Vikunja konto",
"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": "For at fortsætte, skal du indtaste din adgangskode. Du vil modtage en e-mail med yderligere instruktioner.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "Slet min konto",
"requestSuccess": "Anmodningen blev gennemført. Du vil modtage en e-mail med yderligere instruktioner.",
"passwordRequired": "Indtast venligst din adgangskode.",
@ -152,6 +185,7 @@
"scheduled": "Vi sletter din Vikunja-konto {date} ({dateSince}).",
"scheduledCancel": "Klik her for at annullere sletningen af din konto.",
"scheduledCancelText": "For at annullere sletningen af din konto, skal du indtaste din adgangskode nedenfor:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Annuller sletningen af min konto",
"scheduledCancelSuccess": "Vi sletter ikke din konto."
},
@ -225,7 +259,7 @@
"identifier": "Project Identifier",
"identifierPlaceholder": "The project identifier goes here…",
"description": "Description",
"descriptionPlaceholder": "The projects description goes here…",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "Color",
"success": "The project was successfully updated."
},
@ -305,6 +339,9 @@
"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",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favorites"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Titel",
"titlePlaceholder": "Det gemte filters titel skrives her…",
"description": "Beskrivelse",
"descriptionPlaceholder": "Beskrivelsen skrives her…",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"includeNulls": "Inkluder Opgaver som ikke har en værdi indstillet",
"requireAll": "Kræv at alle filtre er sande for at en opgave kan vises",
"showDoneTasks": "Vis Udførte Opgaver",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Du kan godt importere igen, men kan der kan opstå dubletter. Er du sikker?",
"confirm": "Jeg er sikker, start migreringen nu!",
"importUpload": "For at importere data fra {name} til Vikunja, skal du klikke på knappen nedenfor for at vælge en fil.",
"upload": "Upload fil"
"upload": "Upload fil",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etiketter",
@ -444,6 +498,7 @@
"custom": "Brugerdefineret",
"id": "ID",
"created": "Oprettet den",
"createdBy": "Created by {0}",
"actions": "Handlinger",
"cannotBeUndone": "Dette kan ikke fortrydes!"
},
@ -462,24 +517,59 @@
"edit": "Rediger",
"done": "Udfør",
"heading1": "Overskrift 1",
"heading1Tooltip": "Big section heading.",
"heading2": "Overskrift 2",
"heading2Tooltip": "Medium section heading.",
"heading3": "Overskrift 3",
"heading3Tooltip": "Smaller section header.",
"headingSmaller": "Overskrift Mindre",
"headingBigger": "Overskrift Større",
"bold": "Fed",
"italic": "Kursiv",
"strikethrough": "Gennemstreget",
"underline": "Underline",
"code": "Kode",
"codeTooltip": "Capture a code snippet.",
"quote": "Citat",
"unorderedList": "Usorteret liste",
"orderedList": "Ordered List",
"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": "Ryd Blok",
"link": "Link",
"image": "Billede",
"table": "Tabel",
"imageTooltip": "Upload an image from your computer.",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
},
"horizontalRule": "Horisontal Linje",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "Side Om Side",
"guide": "Vejledning"
"guide": "Vejledning",
"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": "Opret ny",
@ -573,6 +663,9 @@
"belongsToProject": "This task belongs to project '{project}'",
"due": "Forfalder {at}",
"closePopup": "Luk pop-up",
"organization": "Organization",
"management": "Management",
"dateAndTime": "Date and time",
"delete": {
"header": "Slet denne opgave",
"text1": "Er du sikker på, at du vil slette denne opgave?",
@ -650,7 +743,7 @@
"loading": "Indlæser kommentarer…",
"edited": "redigeret {date}",
"creating": "Opretter kommentar…",
"placeholder": "Tilføj din kommentar…",
"placeholder": "Add your comment, hit '/' for more options…",
"comment": "Kommentar",
"delete": "Slet denne kommentar",
"deleteText1": "Er du sikker på du vil slette denne kommentar?",
@ -664,7 +757,7 @@
"1week": "1 uge"
},
"description": {
"placeholder": "Klik her for at indtaste en beskrivelse…",
"placeholder": "Enter a description, hit '/' for more options…",
"empty": "Ingen beskrivelse tilgængelig endnu."
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "Hver Dag",
"everyWeek": "Hver Uge",
"everyMonth": "Hver Måned",
"every30d": "Every 30 Days",
"mode": "Gentagelsestilstand",
"monthly": "Månedligt",
"fromCurrentDate": "Fra Nuværende Dato",
@ -815,7 +908,7 @@
"namePlaceholder": "Holdnavnet skrives her…",
"nameRequired": "Angiv venligst et navn.",
"description": "Beskrivelse",
"descriptionPlaceholder": "Holdets beskrivelse skrives her…",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Administrator",
"member": "Medlem"
}
@ -843,7 +936,8 @@
"description": "Slå redigering af opgavebeskrivelse til/fra",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
"favorite": "Mark this task as favorite / unfavorite",
"save": "Save the current task"
},
"project": {
"title": "Project Views",
@ -881,7 +975,7 @@
"urlPlaceholder": "f.eks. https://localhost:3456",
"change": "ændr",
"use": "Brug Vikunja-installationen på {0}",
"error": "Kunne ikke finde eller bruge Vikunja-installationen på \"{domain}\". Prøv venligst en anden 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": "Bruger Vikunja-installationen på \"{domain}\".",
"urlRequired": "En url er påkrævet."
},
@ -893,7 +987,9 @@
"notification": {
"title": "Notifikationer",
"none": "Du har ingen notifikationer. Hav en dejlig dag!",
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
"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": "Kommandoer",
@ -902,6 +998,7 @@
"tasks": "Opgaver",
"projects": "Projects",
"teams": "Hold",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Indtast titlen på den nye opgave…",
"newTeam": "Indtast navnet på det nye hold…",

View File

@ -6,11 +6,17 @@
"welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zuletzt angesehen",
"addToHomeScreen": "Füge diese App deinem Startbildschirm hinzu, um schneller darauf zuzugreifen und das Erlebnis zu verbessern.",
"goToOverview": "Zur Übersicht",
"project": {
"importText": "Importiere deine Projekte und Aufgaben aus anderen Diensten in Vikunja:",
"import": "Importiere deine Daten in Vikunja"
}
},
"demo": {
"title": "Diese Instanz ist im Demo-Modus. Verwende sie nicht mit echten Daten!",
"everythingWillBeDeleted": "Alles wird in regelmäßigen Abständen gelöscht!",
"accountWillBeDeleted": "Dein Account wird gelöscht, einschließlich aller Projekte, Aufgaben und Anhänge, die du möglicherweise erstellst."
},
"404": {
"title": "Nicht gefunden",
"text": "Die angeforderte Seite existiert nicht."
@ -139,12 +145,39 @@
"system": "System",
"dark": "Dunkel"
}
},
"apiTokens": {
"title": "API-Tokens",
"general": "Mit API-Token kannst du die API von Vikunja ohne Login-Daten verwenden.",
"apiDocs": "Schaue dir die API-Dokumentation an",
"createAToken": "Token erstellen",
"createToken": "Token erstellen",
"30d": "30 Tage",
"60d": "60 Tage",
"90d": "90 Tage",
"permissionExplanation": "Mit Berechtigungen kannst du einschränken, was ein API-Token tun darf.",
"titleRequired": "Titel ist erforderlich",
"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!",
"delete": {
"header": "Dieses Token löschen",
"text1": "Bist Du sicher, dass Du das Token \"{token}\" löschen möchtest?",
"text2": "Dies wird den Zugriff des Tokens auf alle Anwendungen oder Integrationen aufheben. Du kannst dies nicht rückgängig machen."
},
"attributes": {
"title": "Titel",
"titlePlaceholder": "Gib einen Titel ein, den du später erkennen wirst",
"expiresAt": "Läuft ab am",
"permissions": "Berechtigungen"
}
}
},
"deletion": {
"title": "Lösche deinen Vikunja-Account",
"text1": "Das Löschen deines Accounts ist dauerhaft und unwiderruflich. Alle Projekte, Aufgaben und zugehörige Daten werden gelöscht.",
"text2": "Zum Fortfahren gib bitte dein Passwort ein. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"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.",
@ -152,6 +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": "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."
},
@ -225,7 +259,7 @@
"identifier": "Projektbezeichner",
"identifierPlaceholder": "Der Projektbezeichner kommt hierhin…",
"description": "Beschreibung",
"descriptionPlaceholder": "Projektbeschreibung eingeben…",
"descriptionPlaceholder": "Gib eine Beschreibung für dieses Projekt ein, drücke '/' für mehr Optionen…",
"color": "Farbe",
"success": "Das Projekt wurde erfolgreich aktualisiert."
},
@ -305,6 +339,9 @@
"doneBucketHint": "Alle Aufgaben, die in diese Spalte verschoben werden, werden automatisch als erledigt markiert.",
"doneBucketHintExtended": "Alle Aufgaben, die in die Erledigt Spalte verschoben wurden, werden automatisch als erledigt markiert. Aufgaben, die in einer anderen Spalte als Erledigt markiert wurden, werden auch in diese Spalte verschoben.",
"doneBucketSavedSuccess": "Erledigt Spalte gespeichert.",
"defaultBucket": "Standard-Spalte",
"defaultBucketHint": "Wenn Aufgaben ohne Angabe einer Spalte erstellt werden, werden sie zu dieser Spalte hinzugefügt.",
"defaultBucketSavedSuccess": "Die Standardspalte wurde erfolgreich gespeichert.",
"deleteLast": "Du kannst die letzte Spalte nicht entfernen.",
"addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
"addTask": "Eine Aufgabe hinzufügen",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favoriten"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Ziel-URL",
"targetUrlInvalid": "Bitte gib eine gültige URL an.",
"events": "Ereignisse",
"eventsHint": "Wähle alle Ereignisse aus, für die dieser Webhook Updates erhalten soll (innerhalb des aktuellen Projekts).",
"mustSelectEvents": "Du musst mindestens ein Ereignis auswählen.",
"delete": "Diesen Webhook löschen",
"deleteText": "Bist du sicher, dass du diesen Webhook löschen möchtest? Externe Ziele werden nicht mehr über Ereignisse benachrichtigt.",
"deleteSuccess": "Der Webhook wurde erfolgreich gelöscht.",
"create": "Webhook erstellen",
"secret": "Schlüssel",
"secretHint": "Wenn angegeben, werden alle Anfragen an die Webhook Ziel-URL mit HMAC signiert.",
"secretDocs": "In der Dokumentation findest du weitere Informationen zum Umgang mit Schlüsseln."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Titel",
"titlePlaceholder": "Einen gespeicherten Filternamen eingeben …",
"description": "Beschreibung",
"descriptionPlaceholder": "Eine Beschreibung eingeben …",
"descriptionPlaceholder": "Gib eine Beschreibung für diesen Filter ein, drücke '/' für mehr Optionen…",
"includeNulls": "Aufgaben ohne Werte einbeziehen",
"requireAll": "Alle Filterkriterien müssen erfüllt sein, damit eine Aufgabe angezeigt wird",
"showDoneTasks": "Erledigte Aufgaben anzeigen",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Ein erneutes Importieren ist möglich, kann aber Duplikate erzeugen. Bist du sicher?",
"confirm": "Ich bin sicher, bitte starte mit der Migration!",
"importUpload": "Um Daten von {name} in Vikunja zu importieren, klicke auf die Schaltfläche unten, um eine Datei auszuwählen.",
"upload": "Datei hochladen"
"upload": "Datei hochladen",
"migrationStartedWillReciveEmail": "Vikunja wird nun deine Listen/Projekte, Aufgaben, Notizen, Erinnerungen und Dateien von {service} importieren. Da dies eine Weile dauern wird, senden wir dir eine E-Mail, sobald der Import abgeschlossen ist. Du kannst dieses Fenster jetzt schließen.",
"migrationInProgress": "Ein Import wird gerade durchgeführt. Bitte warte, bis dieser abgeschlossen ist."
},
"label": {
"title": "Labels",
@ -444,6 +498,7 @@
"custom": "Benutzerdefiniert",
"id": "ID",
"created": "Erstellt am",
"createdBy": "Erstellt von {0}",
"actions": "Aktionen",
"cannotBeUndone": "Dies kann nicht rückgängig gemacht werden!"
},
@ -462,24 +517,59 @@
"edit": "Bearbeiten",
"done": "Fertig",
"heading1": "Überschrift 1",
"heading1Tooltip": "Große Überschrift.",
"heading2": "Überschrift 2",
"heading2Tooltip": "Mittlere Überschrift.",
"heading3": "Überschrift 3",
"heading3Tooltip": "Kleine Überschrift.",
"headingSmaller": "Kleinere Überschrift",
"headingBigger": "Grössere Überschrift",
"bold": "Fett",
"italic": "Kursiv",
"strikethrough": "Durchgestrichen",
"underline": "Unterstrichen",
"code": "Code",
"codeTooltip": "Erfasse ein Code-Snippet.",
"quote": "Zitat",
"quoteTooltip": "Erfasse ein Zitat.",
"bulletList": "Stichpunktliste",
"bulletListTooltip": "Erstelle eine einfache Stichpunktliste.",
"unorderedList": "Ungeordnete Liste",
"orderedList": "Geordnete Liste",
"orderedListTooltip": "Erstelle eine Liste mit Nummerierung.",
"cleanBlock": "Formatierung löschen",
"link": "Link",
"image": "Bild",
"table": "Tabelle",
"imageTooltip": "Lade ein Bild von deinem PC hoch.",
"table": {
"title": "Tabelle",
"insert": "Tabelle einfügen",
"addColumnBefore": "Spalte davor hinzufügen",
"addColumnAfter": "Spalte danach hinzufügen",
"deleteColumn": "Spalte löschen",
"addRowBefore": "Zeile davor hinzufügen",
"addRowAfter": "Zeile danach hinzufügen",
"deleteRow": "Zeile löschen",
"deleteTable": "Tabelle löschen",
"mergeCells": "Zellen verbinden",
"splitCell": "Zelle teilen",
"toggleHeaderColumn": "Headerspalte ein/aus",
"toggleHeaderRow": "Headerzeile ein/aus",
"toggleHeaderCell": "Headerzelle ein/aus",
"mergeOrSplit": "Verbinden oder teilen",
"fixTables": "Tabellen reparieren"
},
"horizontalRule": "Horizontaler Strich",
"horizontalRuleTooltip": "Teile einen Bereich.",
"sideBySide": "Nebeneinander",
"guide": "Hilfslinie"
"guide": "Hilfslinie",
"text": "Text",
"textTooltip": "Einfach einen Text tippen.",
"taskList": "Aufgabenliste",
"taskListTooltip": "Aufgaben mit einer To-do-Liste verfolgen.",
"undo": "Rückgängig",
"redo": "Wiederholen",
"placeholder": "Gib Text ein, drücke '/' für mehr Optionen…"
},
"multiselect": {
"createPlaceholder": "Neu erstellen",
@ -573,6 +663,9 @@
"belongsToProject": "Diese Aufgabe gehört zum Projekt „{project}“",
"due": "Fällig {at}",
"closePopup": "Popup schließen",
"organization": "Organisation",
"management": "Verwaltung",
"dateAndTime": "Datum und Uhrzeit",
"delete": {
"header": "Diese Aufgabe löschen",
"text1": "Willst du diese Aufgabe wirklich löschen?",
@ -650,7 +743,7 @@
"loading": "Kommentare werden geladen …",
"edited": "bearbeitet {date}",
"creating": "Kommentar wird erstellt …",
"placeholder": "Füge deinen Kommentar hinzu …",
"placeholder": "Füge deinen Kommentar hinzu, drücke '/' für weitere Optionen…",
"comment": "Kommentieren",
"delete": "Diesen Kommentar löschen",
"deleteText1": "Bist du sicher, dass du diesen Kommentar löschen willst?",
@ -664,7 +757,7 @@
"1week": "1 Woche"
},
"description": {
"placeholder": "Klicke hier, um eine Beschreibung einzugeben …",
"placeholder": "Gib eine Beschreibung ein, drücke '/' für mehr Optionen…",
"empty": "Noch keine Beschreibung vorhanden."
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "Jeden Tag",
"everyWeek": "Jede Woche",
"everyMonth": "Jeden Monat",
"every30d": "Alle 30 Tage",
"mode": "Wiederholungsmodus",
"monthly": "Monatlich",
"fromCurrentDate": "Ab dem aktuellen Datum",
@ -815,7 +908,7 @@
"namePlaceholder": "Teamname eingeben …",
"nameRequired": "Bitte gib einen Namen an.",
"description": "Beschreibung",
"descriptionPlaceholder": "Die Beschreibung des Teams steht hier …",
"descriptionPlaceholder": "Gib eine Beschreibung für dieses Team ein, drücke '/' für mehr Optionen…",
"admin": "Admin",
"member": "Mitglied"
}
@ -843,7 +936,8 @@
"description": "Aufgabenbeschreibung bearbeiten",
"delete": "Diese Aufgabe löschen",
"priority": "Die Priorität dieser Aufgabe ändern",
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen"
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen",
"save": "Save the current task"
},
"project": {
"title": "Projektansichten",
@ -881,7 +975,7 @@
"urlPlaceholder": "z.B. https://localhost:3456",
"change": "ändern",
"use": "Verwende die Vikunja-Installation unter „{0}“",
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
"error": "Vikunja Installation unter \"{domain}\" konnte nicht gefunden oder verwendet werden. Bitte prüfe, ob die URL das richtige Format hat und direkt darauf zugreifen kannst und versuche es erneut.",
"success": "Verwende die Vikunja-Installation unter „{domain}“.",
"urlRequired": "Eine Url ist erforderlich."
},
@ -893,7 +987,9 @@
"notification": {
"title": "Benachrichtigungen",
"none": "Du hast keine Benachrichtigungen. Einen schönen Tag noch!",
"explainer": "Benachrichtigungen werden hier angezeigt, wenn Aktionen für Projekte oder Aufgaben, die du abonniert hast, ausgeführt werden."
"explainer": "Benachrichtigungen werden hier angezeigt, wenn Aktionen für Projekte oder Aufgaben, die du abonniert hast, ausgeführt werden.",
"markAllRead": "Alle Benachrichtigungen als gelesen markieren",
"markAllReadSuccess": "Alle Benachrichtigungen erfolgreich als gelesen markiert."
},
"quickActions": {
"commands": "Befehle",
@ -902,6 +998,7 @@
"tasks": "Aufgaben",
"projects": "Projekte",
"teams": "Teams",
"labels": "Labels",
"newProject": "Gib den Titel des neuen Projekts ein…",
"newTask": "Gib den Titel der neuen Aufgabe ein …",
"newTeam": "Gib den Namen des neuen Teams ein …",

View File

@ -6,11 +6,17 @@
"welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zletscht ahglueget",
"addToHomeScreen": "Füge diese App deinem Startbildschirm hinzu, um schneller darauf zuzugreifen und das Erlebnis zu verbessern.",
"goToOverview": "Zur Übersicht",
"project": {
"importText": "Importiere deine Projekte und Aufgaben aus anderen Diensten in Vikunja:",
"import": "Importiere deine Daten in Vikunja"
}
},
"demo": {
"title": "Diese Instanz ist im Demo-Modus. Verwende sie nicht mit echten Daten!",
"everythingWillBeDeleted": "Alles wird in regelmäßigen Abständen gelöscht!",
"accountWillBeDeleted": "Dein Account wird gelöscht, einschließlich aller Projekte, Aufgaben und Anhänge, die du möglicherweise erstellst."
},
"404": {
"title": "Nid gfunde",
"text": "Dini gsuechti Siite giz nid."
@ -139,12 +145,39 @@
"system": "System",
"dark": "Dunkel"
}
},
"apiTokens": {
"title": "API-Tokens",
"general": "Mit API-Token kannst du die API von Vikunja ohne Login-Daten verwenden.",
"apiDocs": "Schaue dir die API-Dokumentation an",
"createAToken": "Token erstellen",
"createToken": "Token erstellen",
"30d": "30 Tage",
"60d": "60 Tage",
"90d": "90 Tage",
"permissionExplanation": "Mit Berechtigungen kannst du einschränken, was ein API-Token tun darf.",
"titleRequired": "Titel ist erforderlich",
"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!",
"delete": {
"header": "Dieses Token löschen",
"text1": "Bist Du sicher, dass Du das Token \"{token}\" löschen möchtest?",
"text2": "Dies wird den Zugriff des Tokens auf alle Anwendungen oder Integrationen aufheben. Du kannst dies nicht rückgängig machen."
},
"attributes": {
"title": "Titel",
"titlePlaceholder": "Gib einen Titel ein, den du später erkennen wirst",
"expiresAt": "Läuft ab am",
"permissions": "Berechtigungen"
}
}
},
"deletion": {
"title": "Lösche deinen Vikunja-Account",
"text1": "Das Löschen deines Accounts ist dauerhaft und unwiderruflich. Alle Projekte, Aufgaben und zugehörige Daten werden gelöscht.",
"text2": "Zum Fortfahren gib bitte dein Passwort ein. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"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.",
@ -152,6 +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": "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."
},
@ -225,7 +259,7 @@
"identifier": "Projektbezeichner",
"identifierPlaceholder": "Der Projektbezeichner kommt hierhin…",
"description": "Beschreibung",
"descriptionPlaceholder": "Projektbeschreibung eingeben…",
"descriptionPlaceholder": "Gib eine Beschreibung für dieses Projekt ein, drücke '/' für mehr Optionen…",
"color": "Farbe",
"success": "Das Projekt wurde erfolgreich aktualisiert."
},
@ -305,6 +339,9 @@
"doneBucketHint": "Alle Aufgaben, die in diese Spalte verschoben werden, werden automatisch als erledigt markiert.",
"doneBucketHintExtended": "Alle Aufgaben, die in die Erledigt Spalte verschoben wurden, werden automatisch als erledigt markiert. Aufgaben, die in einer anderen Spalte als Erledigt markiert wurden, werden auch in diese Spalte verschoben.",
"doneBucketSavedSuccess": "Erledigt Spalte gespeichert.",
"defaultBucket": "Standard-Spalte",
"defaultBucketHint": "Wenn Aufgaben ohne Angabe einer Spalte erstellt werden, werden sie zu dieser Spalte hinzugefügt.",
"defaultBucketSavedSuccess": "Die Standardspalte wurde erfolgreich gespeichert.",
"deleteLast": "Du kannst die letzte Spalte nicht entfernen.",
"addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
"addTask": "Eine Aufgabe hinzufügen",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favoriten"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Ziel-URL",
"targetUrlInvalid": "Bitte gib eine gültige URL an.",
"events": "Ereignisse",
"eventsHint": "Wähle alle Ereignisse aus, für die dieser Webhook Updates erhalten soll (innerhalb des aktuellen Projekts).",
"mustSelectEvents": "Du musst mindestens ein Ereignis auswählen.",
"delete": "Diesen Webhook löschen",
"deleteText": "Bist du sicher, dass du diesen Webhook löschen möchtest? Externe Ziele werden nicht mehr über Ereignisse benachrichtigt.",
"deleteSuccess": "Der Webhook wurde erfolgreich gelöscht.",
"create": "Webhook erstellen",
"secret": "Schlüssel",
"secretHint": "Wenn angegeben, werden alle Anfragen an die Webhook Ziel-URL mit HMAC signiert.",
"secretDocs": "In der Dokumentation findest du weitere Informationen zum Umgang mit Schlüsseln."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Titl",
"titlePlaceholder": "De Name für de g'speicheret Filter chunt da ahne…",
"description": "Beschriibig",
"descriptionPlaceholder": "D'Beschriibig chunt da hane…",
"descriptionPlaceholder": "Gib eine Beschreibung für diesen Filter ein, drücke '/' für mehr Optionen…",
"includeNulls": "Uufgabe ohni Wert iihbezieh",
"requireAll": "Alli Filter mend wahr sii, demits die Uufgab ahzeigt",
"showDoneTasks": "Zeig die fertige Uufgabe",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Es erneuts Importiere isch scho mögli, aber chenti Duplikaat erstelle. Bisch der sicher?",
"confirm": "Ich bin sicher, fang mit de Migration ah!",
"importUpload": "Um Daten von {name} in Vikunja zu importieren, klicke auf die Schaltfläche unten, um eine Datei auszuwählen.",
"upload": "Datei hochladen"
"upload": "Datei hochladen",
"migrationStartedWillReciveEmail": "Vikunja wird nun deine Listen/Projekte, Aufgaben, Notizen, Erinnerungen und Dateien von {service} importieren. Da dies eine Weile dauern wird, senden wir dir eine E-Mail, sobald der Import abgeschlossen ist. Du kannst dieses Fenster jetzt schließen.",
"migrationInProgress": "Ein Import wird gerade durchgeführt. Bitte warte, bis dieser abgeschlossen ist."
},
"label": {
"title": "Labels",
@ -444,6 +498,7 @@
"custom": "Benutzerdefiniert",
"id": "ID",
"created": "Erstellt am",
"createdBy": "Erstellt von {0}",
"actions": "Aktionen",
"cannotBeUndone": "Dies kann nicht rückgängig gemacht werden!"
},
@ -462,24 +517,59 @@
"edit": "Bearbeitä",
"done": "Fertig",
"heading1": "Überschrift 1",
"heading1Tooltip": "Große Überschrift.",
"heading2": "Überschrift 2",
"heading2Tooltip": "Mittlere Überschrift.",
"heading3": "Überschrift 3",
"heading3Tooltip": "Kleine Überschrift.",
"headingSmaller": "Chliini Überschrift",
"headingBigger": "Grösseri Überschrift",
"bold": "Fett",
"italic": "Kursiiv",
"strikethrough": "Duregstriche",
"underline": "Unterstrichen",
"code": "Code",
"codeTooltip": "Erfasse ein Code-Snippet.",
"quote": "Zitaat",
"unorderedList": "Ungordnedi Listä",
"quoteTooltip": "Erfasse ein Zitat.",
"bulletList": "Stichpunktliste",
"bulletListTooltip": "Erstelle eine einfache Stichpunktliste.",
"unorderedList": "Ungeordnete Liste",
"orderedList": "Geordnete Liste",
"orderedListTooltip": "Erstelle eine Liste mit Nummerierung.",
"cleanBlock": "Formatierig Lösche",
"link": "Link",
"image": "Bild",
"table": "Tabällä",
"imageTooltip": "Lade ein Bild von deinem PC hoch.",
"table": {
"title": "Tabelle",
"insert": "Tabelle einfügen",
"addColumnBefore": "Spalte davor hinzufügen",
"addColumnAfter": "Spalte danach hinzufügen",
"deleteColumn": "Spalte löschen",
"addRowBefore": "Zeile davor hinzufügen",
"addRowAfter": "Zeile danach hinzufügen",
"deleteRow": "Zeile löschen",
"deleteTable": "Tabelle löschen",
"mergeCells": "Zellen verbinden",
"splitCell": "Zelle teilen",
"toggleHeaderColumn": "Headerspalte ein/aus",
"toggleHeaderRow": "Headerzeile ein/aus",
"toggleHeaderCell": "Headerzelle ein/aus",
"mergeOrSplit": "Verbinden oder teilen",
"fixTables": "Tabellen reparieren"
},
"horizontalRule": "Horizontalä Strich",
"horizontalRuleTooltip": "Teile einen Bereich.",
"sideBySide": "Nebedenand",
"guide": "Hilfsliniä"
"guide": "Hilfsliniä",
"text": "Text",
"textTooltip": "Einfach einen Text tippen.",
"taskList": "Aufgabenliste",
"taskListTooltip": "Aufgaben mit einer To-do-Liste verfolgen.",
"undo": "Rückgängig",
"redo": "Wiederholen",
"placeholder": "Gib Text ein, drücke '/' für mehr Optionen…"
},
"multiselect": {
"createPlaceholder": "Neu erstelle",
@ -573,6 +663,9 @@
"belongsToProject": "Diese Aufgabe gehört zum Projekt „{project}“",
"due": "Fällig bis {at}",
"closePopup": "Popup schließen",
"organization": "Organisation",
"management": "Verwaltung",
"dateAndTime": "Datum und Uhrzeit",
"delete": {
"header": "Die Uufgab chüble",
"text1": "Bisch du dir sicher, dass du die Uufgab chüblä wetsch?",
@ -650,7 +743,7 @@
"loading": "Kommentär werded gladä…",
"edited": "beartbeitet am {date}",
"creating": "Kommentar wird erstellt…",
"placeholder": "Din Kommentar wird hinzuegfüegt…",
"placeholder": "Füge deinen Kommentar hinzu, drücke '/' für weitere Optionen…",
"comment": "Kommentar",
"delete": "De Kommentar chüble",
"deleteText1": "Bisch du dir sicher, dass du de Kommentar chüble wetsch?",
@ -664,7 +757,7 @@
"1week": "Ei Wuche"
},
"description": {
"placeholder": "Klicke do, um e Beschriibig iihzfüege…",
"placeholder": "Gib eine Beschreibung ein, drücke '/' für mehr Optionen…",
"empty": "Momentan hets kei Beschriibig."
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "Jedä Tag",
"everyWeek": "Jedi Wuche",
"everyMonth": "Jede Monet",
"every30d": "Alle 30 Tage",
"mode": "Widerholigs Modus",
"monthly": "Monatlich",
"fromCurrentDate": "Vom Hüttige Datum",
@ -815,7 +908,7 @@
"namePlaceholder": "Teamname da ahgeh…",
"nameRequired": "Bitte gib en Name an.",
"description": "Beschriibig",
"descriptionPlaceholder": "D'Team Beschriibig chunt da ahne…",
"descriptionPlaceholder": "Gib eine Beschreibung für dieses Team ein, drücke '/' für mehr Optionen…",
"admin": "Chef",
"member": "Mitglied"
}
@ -843,7 +936,8 @@
"description": "Aufgabenbeschreibung bearbeiten",
"delete": "Diese Aufgabe löschen",
"priority": "Die Priorität dieser Aufgabe ändern",
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen"
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen",
"save": "Save the current task"
},
"project": {
"title": "Projektansichten",
@ -881,7 +975,7 @@
"urlPlaceholder": "z.B. https://localhost:3456",
"change": "ändere",
"use": "Verwende die Vikunja-Installation unter „{0}“",
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
"error": "Vikunja Installation unter \"{domain}\" konnte nicht gefunden oder verwendet werden. Bitte prüfe, ob die URL das richtige Format hat und direkt darauf zugreifen kannst und versuche es erneut.",
"success": "Benutze d'Vikunja Installation uf \"{domain}\".",
"urlRequired": "Eine Url ist erforderlich."
},
@ -893,7 +987,9 @@
"notification": {
"title": "Benachrichtigunge",
"none": "Du hesch kei neui Benachrichtunge. Heb e schös Tägli!",
"explainer": "Benachrichtigungen werden hier angezeigt, wenn Aktionen für Projekte oder Aufgaben, die du abonniert hast, ausgeführt werden."
"explainer": "Benachrichtigungen werden hier angezeigt, wenn Aktionen für Projekte oder Aufgaben, die du abonniert hast, ausgeführt werden.",
"markAllRead": "Alle Benachrichtigungen als gelesen markieren",
"markAllReadSuccess": "Alle Benachrichtigungen erfolgreich als gelesen markiert."
},
"quickActions": {
"commands": "Befehl",
@ -902,6 +998,7 @@
"tasks": "Uufgabe",
"projects": "Projekte",
"teams": "Teams",
"labels": "Labels",
"newProject": "Gib den Titel des neuen Projekts ein…",
"newTask": "Gib en Titl für die neu Uufgab iih…",
"newTeam": "Gib en Name für da neui Team iih…",

View File

@ -6,11 +6,17 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -139,12 +145,39 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
"title": "Delete your Vikunja Account",
"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": "To proceed, please enter your password. You will receive an email with further instructions.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "Delete my account",
"requestSuccess": "The request was successful. You'll receive an email with further instructions.",
"passwordRequired": "Please enter your password.",
@ -152,6 +185,7 @@
"scheduled": "We will delete your Vikunja account at {date} ({dateSince}).",
"scheduledCancel": "To cancel the deletion of your account, click here.",
"scheduledCancelText": "To cancel the deletion of your account, please enter your password below:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Cancel the deletion of my account",
"scheduledCancelSuccess": "We will not delete your account."
},
@ -225,7 +259,7 @@
"identifier": "Project Identifier",
"identifierPlaceholder": "The project identifier goes here…",
"description": "Description",
"descriptionPlaceholder": "The projects description goes here…",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "Color",
"success": "The project was successfully updated."
},
@ -305,6 +339,9 @@
"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",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favorites"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
"description": "Description",
"descriptionPlaceholder": "The description goes here…",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"includeNulls": "Include Tasks which don't have a value set",
"requireAll": "Require all filters to be true for a task to show up",
"showDoneTasks": "Show Done Tasks",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",
@ -444,6 +498,7 @@
"custom": "Custom",
"id": "ID",
"created": "Created at",
"createdBy": "Created by {0}",
"actions": "Actions",
"cannotBeUndone": "This cannot be undone!"
},
@ -462,24 +517,60 @@
"edit": "Edit",
"done": "Done",
"heading1": "Heading 1",
"heading1Tooltip": "Big section heading.",
"heading2": "Heading 2",
"heading2Tooltip": "Medium section heading.",
"heading3": "Heading 3",
"heading3Tooltip": "Smaller section header.",
"headingSmaller": "Heading Smaller",
"headingBigger": "Heading Bigger",
"bold": "Bold",
"italic": "Italic",
"strikethrough": "Strikethrough",
"underline": "Underline",
"code": "Code",
"codeTooltip": "Capture a code snippet.",
"quote": "Quote",
"unorderedList": "Unordered List",
"orderedList": "Ordered List",
"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": "Clean Block",
"link": "Link",
"image": "Image",
"imageTooltip": "Upload an image from your computer.",
"table": "Table",
"horizontalRule": "Horizontal Rule",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "Side By Side",
"guide": "Guide"
"guide": "Guide",
"text": "Text",
"textTooltip": "Just start typing with plain text.",
"taskList": "Task list",
"taskListTooltip": "Track tasks with a to-do list.",
"undo": "Undo",
"redo": "Redo",
"placeholder": "Type some text or hit '/' to see more options…",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
}
},
"multiselect": {
"createPlaceholder": "Create new",
@ -576,6 +667,9 @@
"belongsToProject": "This task belongs to project '{project}'",
"due": "Due {at}",
"closePopup": "Close popup",
"organization": "Organization",
"management": "Management",
"dateAndTime": "Date and time",
"delete": {
"header": "Delete this task",
"text1": "Are you sure you want to remove this task?",
@ -653,7 +747,7 @@
"loading": "Loading comments…",
"edited": "edited {date}",
"creating": "Creating comment…",
"placeholder": "Add your comment…",
"placeholder": "Add your comment, hit '/' for more options…",
"comment": "Comment",
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
@ -667,7 +761,7 @@
"1week": "1 week"
},
"description": {
"placeholder": "Click here to enter a description…",
"placeholder": "Enter a description, hit '/' for more options…",
"empty": "No description available yet."
},
"assignee": {
@ -736,7 +830,7 @@
"repeat": {
"everyDay": "Every Day",
"everyWeek": "Every Week",
"everyMonth": "Every Month",
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From Current Date",
@ -818,7 +912,7 @@
"namePlaceholder": "The team's name goes here…",
"nameRequired": "Please specify a name.",
"description": "Description",
"descriptionPlaceholder": "The teams description goes here…",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
}
@ -846,7 +940,8 @@
"description": "Toggle editing of the task description",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
"favorite": "Mark this task as favorite / unfavorite",
"save": "Save the current task"
},
"project": {
"title": "Project Views",
@ -884,7 +979,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different 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": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -896,7 +991,9 @@
"notification": {
"title": "Notifications",
"none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
"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": "Commands",
@ -905,6 +1002,7 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -6,11 +6,17 @@
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -139,12 +145,39 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
"title": "Delete your Vikunja Account",
"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": "To proceed, please enter your password. You will receive an email with further instructions.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "Delete my account",
"requestSuccess": "The request was successful. You'll receive an email with further instructions.",
"passwordRequired": "Please enter your password.",
@ -152,6 +185,7 @@
"scheduled": "We will delete your Vikunja account at {date} ({dateSince}).",
"scheduledCancel": "To cancel the deletion of your account, click here.",
"scheduledCancelText": "To cancel the deletion of your account, please enter your password below:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Cancel the deletion of my account",
"scheduledCancelSuccess": "We will not delete your account."
},
@ -225,7 +259,7 @@
"identifier": "Project Identifier",
"identifierPlaceholder": "The project identifier goes here…",
"description": "Description",
"descriptionPlaceholder": "The projects description goes here…",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "Color",
"success": "The project was successfully updated."
},
@ -305,6 +339,9 @@
"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",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favorites"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
"description": "Description",
"descriptionPlaceholder": "The description goes here…",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"includeNulls": "Include Tasks which don't have a value set",
"requireAll": "Require all filters to be true for a task to show up",
"showDoneTasks": "Show Done Tasks",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Upload file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Labels",
@ -444,6 +498,7 @@
"custom": "Custom",
"id": "ID",
"created": "Created at",
"createdBy": "Created by {0}",
"actions": "Actions",
"cannotBeUndone": "This cannot be undone!"
},
@ -462,24 +517,59 @@
"edit": "Edit",
"done": "Done",
"heading1": "Heading 1",
"heading1Tooltip": "Big section heading.",
"heading2": "Heading 2",
"heading2Tooltip": "Medium section heading.",
"heading3": "Heading 3",
"heading3Tooltip": "Smaller section header.",
"headingSmaller": "Heading Smaller",
"headingBigger": "Heading Bigger",
"bold": "Bold",
"italic": "Italic",
"strikethrough": "Strikethrough",
"underline": "Underline",
"code": "Code",
"codeTooltip": "Capture a code snippet.",
"quote": "Quote",
"unorderedList": "Unordered List",
"orderedList": "Ordered List",
"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": "Clean Block",
"link": "Link",
"image": "Image",
"table": "Table",
"imageTooltip": "Upload an image from your computer.",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
},
"horizontalRule": "Horizontal Rule",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "Side By Side",
"guide": "Guide"
"guide": "Guide",
"text": "Text",
"textTooltip": "Just start typing with plain text.",
"taskList": "Task list",
"taskListTooltip": "Track tasks with a to-do list.",
"undo": "Undo",
"redo": "Redo",
"placeholder": "Type some text or hit '/' to see more options…"
},
"multiselect": {
"createPlaceholder": "Create new",
@ -573,6 +663,9 @@
"belongsToProject": "This task belongs to project '{project}'",
"due": "Due {at}",
"closePopup": "Close popup",
"organization": "Organization",
"management": "Management",
"dateAndTime": "Date and time",
"delete": {
"header": "Delete this task",
"text1": "Are you sure you want to remove this task?",
@ -650,7 +743,7 @@
"loading": "Loading comments…",
"edited": "edited {date}",
"creating": "Creating comment…",
"placeholder": "Add your comment…",
"placeholder": "Add your comment, hit '/' for more options…",
"comment": "Comment",
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
@ -664,7 +757,7 @@
"1week": "1 week"
},
"description": {
"placeholder": "Click here to enter a description…",
"placeholder": "Enter a description, hit '/' for more options…",
"empty": "No description available yet."
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "Every Day",
"everyWeek": "Every Week",
"everyMonth": "Every Month",
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From Current Date",
@ -815,7 +908,7 @@
"namePlaceholder": "The team's name goes here…",
"nameRequired": "Please specify a name.",
"description": "Description",
"descriptionPlaceholder": "The teams description goes here…",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
}
@ -843,7 +936,8 @@
"description": "Toggle editing of the task description",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
"favorite": "Mark this task as favorite / unfavorite",
"save": "Save the current task"
},
"project": {
"title": "Project Views",
@ -881,7 +975,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different 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": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -893,7 +987,9 @@
"notification": {
"title": "Notifications",
"none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
"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": "Commands",
@ -902,6 +998,7 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -6,11 +6,17 @@
"welcomeEvening": "¡Buenas Tardes {username}!",
"lastViewed": "Visto por última vez",
"addToHomeScreen": "Añade esta aplicación a tu pantalla de inicio para un acceso más rápido y una experiencia mejorada.",
"goToOverview": "Go to overview",
"project": {
"importText": "Importa tus proyectos y tareas de otros servicios a Vikunja:",
"import": "Importa tus datos a Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "No encontrado",
"text": "La página solicitada no existe."
@ -139,12 +145,39 @@
"system": "Sistema",
"dark": "Oscuro"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
"title": "Eliminar tu Cuenta de Vikunja",
"text1": "El borrado de tu cuenta es permanente y no puede deshacerse. Borraremos todos tus proyectos, tareas y todo lo que esté asociado con ellos.",
"text2": "Para continuar, por favor, introduce tu contraseña. Recibirás un correo electrónico con más instrucciones.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "Eliminar mi cuenta",
"requestSuccess": "La solicitud ha sido exitosa. Recibirás un correo electrónico con más instrucciones.",
"passwordRequired": "Por favor, introduce tu contraseña.",
@ -152,6 +185,7 @@
"scheduled": "Eliminaremos tu cuenta de Vikunja en {date} ({dateSince}).",
"scheduledCancel": "Para cancelar la eliminación de tu cuenta, haz clic aquí.",
"scheduledCancelText": "Para cancelar la eliminación de tu cuenta, por favor, introduce tu contraseña a continuación:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Cancelar la eliminación de mi cuenta",
"scheduledCancelSuccess": "No eliminaremos tu cuenta."
},
@ -225,7 +259,7 @@
"identifier": "Identificador del proyecto",
"identifierPlaceholder": "El identificador del proyecto va aquí…",
"description": "Descripción",
"descriptionPlaceholder": "La descripción del proyecto va aquí…",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "Color",
"success": "El proyecto se ha actualizado con éxito."
},
@ -305,6 +339,9 @@
"doneBucketHint": "Todas las tareas movidas a este contenedor se marcarán automáticamente como finalizadas.",
"doneBucketHintExtended": "Todas las tareas movidas al contenedor completado se marcarán como finalizadas automáticamente. Todas las tareas marcadas como finalizadas desde otro lugar también se moverán.",
"doneBucketSavedSuccess": "El contenedor completado se ha guardado correctamente.",
"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": "No puedes eliminar el último contenedor.",
"addTaskPlaceholder": "Introduce el nuevo título de la tarea…",
"addTask": "Añadir una tarea",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favoritos"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Título",
"titlePlaceholder": "El título del filtro guardado va aquí…",
"description": "Descripción",
"descriptionPlaceholder": "La descripción va aquí…",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"includeNulls": "Incluye tareas que no tienen un valor establecido",
"requireAll": "Requerir que todos los filtros sean verdaderos para que una tarea se muestre",
"showDoneTasks": "Mostrar tareas completadas",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Importar de nuevo es posible, pero puede crear duplicados. ¿Estás seguro?",
"confirm": "Estoy seguro, ¡por favor empieza a migrar ahora!",
"importUpload": "Para importar datos de {name} a Vikunja, haz clic en el botón de abajo para seleccionar un archivo.",
"upload": "Subir archivo"
"upload": "Subir archivo",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etiquetas",
@ -444,6 +498,7 @@
"custom": "Personalizado",
"id": "ID",
"created": "Creado en",
"createdBy": "Created by {0}",
"actions": "Acciones",
"cannotBeUndone": "¡No se puede deshacer!"
},
@ -462,24 +517,59 @@
"edit": "Editar",
"done": "Hecho",
"heading1": "Encabezado 1",
"heading1Tooltip": "Big section heading.",
"heading2": "Encabezado 2",
"heading2Tooltip": "Medium section heading.",
"heading3": "Encabezado 3",
"heading3Tooltip": "Smaller section header.",
"headingSmaller": "Reducir Encabezado",
"headingBigger": "Aumentar Encabezado",
"bold": "Negrita",
"italic": "Cursiva",
"strikethrough": "Tachado",
"underline": "Underline",
"code": "Código",
"codeTooltip": "Capture a code snippet.",
"quote": "Cita",
"unorderedList": "Lista no ordenada",
"orderedList": "Lista ordenada",
"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": "Borrar Bloque",
"link": "Enlace",
"image": "Imagen",
"table": "Tabla",
"imageTooltip": "Upload an image from your computer.",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
},
"horizontalRule": "Regla Horizontal",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "De Lado a Lado",
"guide": "Guía"
"guide": "Guía",
"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": "Crear nuevo",
@ -573,6 +663,9 @@
"belongsToProject": "Esta tarea pertenece al proyecto '{project}'",
"due": "Vence {at}",
"closePopup": "Cerrar ventana emergente",
"organization": "Organization",
"management": "Management",
"dateAndTime": "Date and time",
"delete": {
"header": "Eliminar esta tarea",
"text1": "¿Está seguro que desea eliminar esta tarea?",
@ -650,7 +743,7 @@
"loading": "Cargando comentarios…",
"edited": "editado {date}",
"creating": "Creando comentario…",
"placeholder": "Añade tu comentario…",
"placeholder": "Add your comment, hit '/' for more options…",
"comment": "Comentar",
"delete": "Eliminar este comentario",
"deleteText1": "¿Está seguro que desea eliminar este comentario?",
@ -664,7 +757,7 @@
"1week": "1 semana"
},
"description": {
"placeholder": "Presione aquí para añadir una descripción…",
"placeholder": "Enter a description, hit '/' for more options…",
"empty": "Aún no hay descripción disponible."
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "Cada Día",
"everyWeek": "Cada Semana",
"everyMonth": "Cada Mes",
"every30d": "Every 30 Days",
"mode": "Modo de repetición",
"monthly": "Mensualmente",
"fromCurrentDate": "Desde la Fecha Actual",
@ -815,7 +908,7 @@
"namePlaceholder": "El nombre del equipo va aquí…",
"nameRequired": "Por favor especifica un nombre.",
"description": "Descripción",
"descriptionPlaceholder": "La descripción del equipo va aquí…",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Miembro"
}
@ -843,7 +936,8 @@
"description": "Editar la descripción de la tarea",
"delete": "Eliminar esta tarea",
"priority": "Cambiar la prioridad de esta tarea",
"favorite": "Marcar esta tarea como favorita / no favorita"
"favorite": "Marcar esta tarea como favorita / no favorita",
"save": "Save the current task"
},
"project": {
"title": "Vistas de proyecto",
@ -881,7 +975,7 @@
"urlPlaceholder": "ej. https://localhost:3456",
"change": "cambiar",
"use": "Utilizando la instalación de Vikunja en {0}",
"error": "No se pudo encontrar o usar la instalación de Vikunja en \"{domain}\". Por favor, prueba con una url diferente.",
"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": "Usando la instalación de Vikunja en \"{domain}\".",
"urlRequired": "Se requiere una url."
},
@ -893,7 +987,9 @@
"notification": {
"title": "Notificaciones",
"none": "No tienes notificaciones. ¡Que tengas un buen día!",
"explainer": "Las notificaciones aparecerán aquí cuando se realicen acciones en proyectos o tareas a las que te hayas suscrito."
"explainer": "Las notificaciones aparecerán aquí cuando se realicen acciones en proyectos o tareas a las que te hayas suscrito.",
"markAllRead": "Mark all notifications as read",
"markAllReadSuccess": "Successfully marked all notifications as read."
},
"quickActions": {
"commands": "Comandos",
@ -902,6 +998,7 @@
"tasks": "Tareas",
"projects": "Proyectos",
"teams": "Equipos",
"labels": "Labels",
"newProject": "Introduzca el título del nuevo proyecto…",
"newTask": "Introduzca el título de la nueva tarea…",
"newTeam": "Introduzca el nombre del nuevo equipo…",

View File

@ -6,11 +6,17 @@
"welcomeEvening": "Bonsoir {username} !",
"lastViewed": "Dernière consultation",
"addToHomeScreen": "Ajoutez cette application à votre écran d'accueil pour un accès plus rapide et une meilleure expérience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Importer vos projets et tâches dautres services dans Vikunja :",
"import": "Importer vos données dans Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Non trouvé",
"text": "La page que vous avez demandée nexiste pas."
@ -139,12 +145,39 @@
"system": "Système",
"dark": "Sombre"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
"title": "Supprimer votre compte Vikunja",
"text1": "La suppression de votre compte est définitive et ne peut pas être annulée. Nous supprimerons tous vos projets, tâches et tout ce qui y est associé.",
"text2": "Pour continuer, saisissez votre mot de passe. Vous recevrez un courriel contenant les instructions à suivre.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "Supprimer mon compte",
"requestSuccess": "La requête a abouti. Vous recevrez un courriel avec des instructions à suivre.",
"passwordRequired": "Saisissez votre mot de passe.",
@ -152,6 +185,7 @@
"scheduled": "Nous allons supprimer votre compte Vikunja le {date} ({dateSince}).",
"scheduledCancel": "Pour annuler la suppression de votre compte, cliquez ici.",
"scheduledCancelText": "Pour annuler la suppression de votre compte, saisissez votre mot de passe ci-dessous :",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Annuler la suppression de mon compte",
"scheduledCancelSuccess": "Nous ne supprimerons pas votre compte."
},
@ -225,7 +259,7 @@
"identifier": "Identifiant de projet",
"identifierPlaceholder": "Saisir ici lidentifiant du projet…",
"description": "Description",
"descriptionPlaceholder": "Saisir ici la description du projet…",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "Couleur",
"success": "Le projet a bien été mis à jour."
},
@ -305,6 +339,9 @@
"doneBucketHint": "Toute tâche déplacée dans cette colonne sera automatiquement marquée comme terminée.",
"doneBucketHintExtended": "Toute tâche déplacée dans cette colonne sera automatiquement marquée comme terminée. Toute tâche marquée comme terminée ailleurs sera également déplacée.",
"doneBucketSavedSuccess": "La colonne des tâches terminées a bien été enregistrée.",
"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": "Vous ne pouvez pas retirer la dernière colonne.",
"addTaskPlaceholder": "Saisir le nouveau nom de la tâche…",
"addTask": "Ajouter une tâche",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favoris"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Nom",
"titlePlaceholder": "Entre un nom de filtre enregistré…",
"description": "Description",
"descriptionPlaceholder": "Écris une description…",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"includeNulls": "Inclure les tâches sans valeurs",
"requireAll": "Exiger tous les filtres pour quune tâche saffiche",
"showDoneTasks": "Afficher les tâches terminées",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Importer à nouveau est possible mais peut créer des doublons. Es-tu sûr·e ?",
"confirm": "Je suis sûr·e, commencer à migrer maintenant !",
"importUpload": "Pour importer les données de {name} dans Vikunja, cliquez sur le bouton ci-dessous pour sélectionner un fichier.",
"upload": "Téléverser le fichier"
"upload": "Téléverser le fichier",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Étiquettes",
@ -444,6 +498,7 @@
"custom": "Personnaliser",
"id": "Identifiant",
"created": "Créé à",
"createdBy": "Created by {0}",
"actions": "Actions",
"cannotBeUndone": "Cette action ne peut pas être annulée !"
},
@ -462,24 +517,59 @@
"edit": "Modifier",
"done": "Terminé",
"heading1": "En-tête 1",
"heading1Tooltip": "Big section heading.",
"heading2": "En-tête 2",
"heading2Tooltip": "Medium section heading.",
"heading3": "En-tête 3",
"heading3Tooltip": "Smaller section header.",
"headingSmaller": "En-tête plus petit",
"headingBigger": "En-tête plus grand",
"bold": "Gras",
"italic": "Italique",
"strikethrough": "Barré",
"underline": "Underline",
"code": "Code",
"codeTooltip": "Capture a code snippet.",
"quote": "Citation",
"unorderedList": "Liste non ordonnée",
"orderedList": "Liste ordonnée",
"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": "Nettoyer le code",
"link": "Lien",
"image": "Image",
"table": "Tableau",
"imageTooltip": "Upload an image from your computer.",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
},
"horizontalRule": "Règle horizontale",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "Côte à côte",
"guide": "Guide"
"guide": "Guide",
"text": "Text",
"textTooltip": "Just start typing with plain text.",
"taskList": "Task list",
"taskListTooltip": "Track tasks with a to-do list.",
"undo": "Undo",
"redo": "Redo",
"placeholder": "Type some text or hit '/' to see more options…"
},
"multiselect": {
"createPlaceholder": "Créer un nouveau",
@ -573,6 +663,9 @@
"belongsToProject": "Cette tâche appartient au projet « {project} »",
"due": "Échéance {at}",
"closePopup": "Fermer la fenêtre",
"organization": "Organization",
"management": "Management",
"dateAndTime": "Date and time",
"delete": {
"header": "Supprimer cette tâche",
"text1": "Retirer cette tâche ?",
@ -650,7 +743,7 @@
"loading": "Chargement des commentaires…",
"edited": "modifié {date}",
"creating": "Création dun commentaire…",
"placeholder": "Ajouter votre commentaire…",
"placeholder": "Add your comment, hit '/' for more options…",
"comment": "Commentaire",
"delete": "Supprimer ce commentaire",
"deleteText1": "Supprimer ce commentaire ?",
@ -664,7 +757,7 @@
"1week": "1 semaine"
},
"description": {
"placeholder": "Cliquez ici pour entrer une description…",
"placeholder": "Enter a description, hit '/' for more options…",
"empty": "Aucune description nest encore disponible."
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "Chaque jour",
"everyWeek": "Chaque semaine",
"everyMonth": "Chaque mois",
"every30d": "Every 30 Days",
"mode": "Mode de répétition",
"monthly": "Mensuel",
"fromCurrentDate": "À partir de la date actuelle",
@ -815,7 +908,7 @@
"namePlaceholder": "Saisir le nom de léquipe…",
"nameRequired": "Indiquer un nom.",
"description": "Description",
"descriptionPlaceholder": "Saisir la description des équipes…",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Membre"
}
@ -843,7 +936,8 @@
"description": "Activer ou désactiver la modification de la description de tâche",
"delete": "Supprimer cette tâche",
"priority": "Changer la priorité de cette tâche",
"favorite": "Marquer cette tâche comme favorite ou non"
"favorite": "Marquer cette tâche comme favorite ou non",
"save": "Save the current task"
},
"project": {
"title": "Vues du projet",
@ -881,7 +975,7 @@
"urlPlaceholder": "Par exemple : https://localhost:3456",
"change": "changer",
"use": "Utiliser linstallation de Vikunja à {0}",
"error": "Impossible de trouver ou d'utiliser l'installation de Vikunja sur « {domain} ». Veuillez essayer une autre adresse.",
"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": "Utilisation de linstallation Vikunja sur « {domain} ».",
"urlRequired": "Une adresse est requise."
},
@ -893,7 +987,9 @@
"notification": {
"title": "Notifications",
"none": "Vous navez pas de notification. Passez une bonne journée !",
"explainer": "Les notifications apparaîtront ici lorsque des actions auxquelles vous êtes abonné·e se produisent."
"explainer": "Les notifications apparaîtront ici lorsque des actions auxquelles vous êtes abonné·e se produisent.",
"markAllRead": "Mark all notifications as read",
"markAllReadSuccess": "Successfully marked all notifications as read."
},
"quickActions": {
"commands": "Commandes",
@ -902,6 +998,7 @@
"tasks": "Tâches",
"projects": "Projets",
"teams": "Équipes",
"labels": "Labels",
"newProject": "Saisissez le nom du nouveau projet…",
"newTask": "Saisir le nom de la nouvelle tâche…",
"newTeam": "Saisir le nom de la nouvelle équipe…",

1105
src/i18n/lang/hu-HU.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,18 @@
"welcomeDay": "Ciao {username}!",
"welcomeEvening": "Buonasera {username}!",
"lastViewed": "Ultima visualizzazione",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"addToHomeScreen": "Aggiungi questa app alla tua schermata iniziale per un accesso più veloce e un'esperienza migliore.",
"goToOverview": "Go to overview",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Importa i tuoi dati in Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Non trovato",
"text": "La pagina richiesta non esiste."
@ -139,12 +145,39 @@
"system": "Sistema",
"dark": "Scuro"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
"title": "Elimina il tuo Account Vikunja",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
"text2": "Per continuare, inserisci la tua password. Riceverai un'e-mail con ulteriori istruzioni.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "Elimina il mio profilo",
"requestSuccess": "Richiesta riuscita. Riceverai un'e-mail con ulteriori istruzioni.",
"passwordRequired": "Inserisci la tua password.",
@ -152,6 +185,7 @@
"scheduled": "Elimineremo il tuo account Vikunja il {date} ({dateSince}).",
"scheduledCancel": "Per annullare l'eliminazione del tuo account, clicca qui.",
"scheduledCancelText": "Per annullare l'eliminazione del tuo account, inserisci la password qui sotto:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Annulla l'eliminazione del mio account",
"scheduledCancelSuccess": "Non elimineremo il tuo account."
},
@ -166,8 +200,8 @@
},
"project": {
"archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archived",
"showArchived": "Show Archived",
"archived": "Archiviati",
"showArchived": "Mostra Archiviati",
"title": "Titolo Progetto",
"color": "Colore",
"projects": "Progetti",
@ -225,7 +259,7 @@
"identifier": "Project Identifier",
"identifierPlaceholder": "The project identifier goes here…",
"description": "Descrizione",
"descriptionPlaceholder": "Descrizione del progetto…",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "Colore",
"success": "The project was successfully updated."
},
@ -305,6 +339,9 @@
"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",
@ -323,6 +360,21 @@
"favorites": {
"title": "Favorites"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
}
},
"filters": {
@ -332,7 +384,7 @@
"title": "Titolo",
"titlePlaceholder": "Il titolo del filtro salvato va qui…",
"description": "Descrizione",
"descriptionPlaceholder": "La descrizione va qui…",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"includeNulls": "Includi attività che non hanno un valore impostato",
"requireAll": "Tutti i filtri devono essere veri affinché l'attività venga mostrata",
"showDoneTasks": "Mostra Attività Fatte",
@ -373,7 +425,9 @@
"alreadyMigrated2": "Importare di nuovo è possibile, ma potrebbe creare duplicati. Sei sicuro?",
"confirm": "Sono sicuro, per favore inizia adesso la migrazione!",
"importUpload": "Per importare i dati da {name} in Vikunja, fai clic sul pulsante qui sotto per selezionare un file.",
"upload": "Carica file"
"upload": "Carica file",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "Etichette",
@ -444,6 +498,7 @@
"custom": "Personalizzato",
"id": "ID",
"created": "Creato il",
"createdBy": "Created by {0}",
"actions": "Azioni",
"cannotBeUndone": "Questa azione non può essere annullata!"
},
@ -462,24 +517,59 @@
"edit": "Modifica",
"done": "Fatto",
"heading1": "Intestazione 1",
"heading1Tooltip": "Big section heading.",
"heading2": "Intestazione 2",
"heading2Tooltip": "Medium section heading.",
"heading3": "Intestazione 3",
"heading3Tooltip": "Smaller section header.",
"headingSmaller": "Intestazione Più Piccola",
"headingBigger": "Intestazione Più Grande",
"bold": "Grassetto",
"italic": "Corsivo",
"strikethrough": "Barrato",
"underline": "Underline",
"code": "Codice",
"codeTooltip": "Capture a code snippet.",
"quote": "Citazione",
"unorderedList": "Elenco puntato",
"orderedList": "Ordered List",
"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": "Pulisci Blocco",
"link": "Link",
"image": "Immagine",
"table": "Tabella",
"imageTooltip": "Upload an image from your computer.",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
},
"horizontalRule": "Divisore Orizzontale",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "Affianca",
"guide": "Guida"
"guide": "Guida",
"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": "Crea nuovo",
@ -573,6 +663,9 @@
"belongsToProject": "This task belongs to project '{project}'",
"due": "Scadenza {at}",
"closePopup": "Chiudi popup",
"organization": "Organization",
"management": "Management",
"dateAndTime": "Date and time",
"delete": {
"header": "Elimina questa attività",
"text1": "Sei sicuro di voler eliminare questa attività?",
@ -650,7 +743,7 @@
"loading": "Caricamento commenti…",
"edited": "modificato il {date}",
"creating": "Creazione del commento…",
"placeholder": "Aggiungi un commento…",
"placeholder": "Add your comment, hit '/' for more options…",
"comment": "Commenta",
"delete": "Elimina questo commento",
"deleteText1": "Sei sicuro di voler eliminare questo commento?",
@ -664,7 +757,7 @@
"1week": "1 settimana"
},
"description": {
"placeholder": "Clicca qui per inserire una descrizione…",
"placeholder": "Enter a description, hit '/' for more options…",
"empty": "Nessuna descrizione."
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "Ogni Giorno",
"everyWeek": "Ogni Settimana",
"everyMonth": "Ogni Mese",
"every30d": "Every 30 Days",
"mode": "Modalità Ripetizione",
"monthly": "Mensilmente",
"fromCurrentDate": "Dalla Data Attuale",
@ -815,7 +908,7 @@
"namePlaceholder": "Il nome del gruppo va qui…",
"nameRequired": "Specifica un nome.",
"description": "Descrizione",
"descriptionPlaceholder": "La descrizione del gruppo va qui…",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Amministratore",
"member": "Membro"
}
@ -843,7 +936,8 @@
"description": "Attiva/Disattiva modifica della descrizione dell'attività",
"delete": "Elimina questa attività",
"priority": "Modifica la priorità di questa attività",
"favorite": "Segna questa attività come preferita o non preferita"
"favorite": "Segna questa attività come preferita o non preferita",
"save": "Save the current task"
},
"project": {
"title": "Project Views",
@ -881,7 +975,7 @@
"urlPlaceholder": "es. http://localhost:8080",
"change": "modifica",
"use": "Usa l'installazione di Vikunja a {0}",
"error": "Impossibile trovare o usare l'installazione di Vikunja su \"{domain}\". Prova per favore con un altro Url.",
"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": "Utilizzando l'installazione di Vikunja su \"{domain}\".",
"urlRequired": "L'URL è obbligatorio."
},
@ -893,7 +987,9 @@
"notification": {
"title": "Notifiche",
"none": "Nessuna notifica. Buona giornata!",
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
"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": "Comandi",
@ -902,6 +998,7 @@
"tasks": "Attivitá",
"projects": "Projects",
"teams": "Gruppi",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Inserisci il titolo della nuova attività…",
"newTeam": "Inserisci il nome del nuovo gruppo…",

View File

@ -6,11 +6,17 @@
"welcomeEvening": "こんばんは、{username}さん",
"lastViewed": "最近の表示",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "他のサービスからVikunjaにプロジェクトやタスクをインポートします:",
"import": "Vikunjaへのデータのインポート"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "リクエストされたページは存在しません。"
@ -87,7 +93,7 @@
"defaultProject": "デフォルトのプロジェクト",
"timezone": "タイムゾーン",
"overdueTasksRemindersTime": "期限切れタスクのリマインダー送信時間",
"filterUsedOnOverview": "Saved filter used on the overview page"
"filterUsedOnOverview": "概要ページに使用される絞り込み条件を保存しました"
},
"totp": {
"title": "2要素認証",
@ -139,12 +145,39 @@
"system": "システム既定",
"dark": "ダーク"
}
},
"apiTokens": {
"title": "APIトークン",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30日",
"60d": "60日",
"90d": "90日",
"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": "このトークンは二度と表示されません。安全な場所に保管してください。",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "トークン名",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
"title": "Vikunjaアカウントの削除",
"text1": "アカウントの削除は永久的なものであり、元に戻すことはできません。あなたのプロジェクト、タスク、それらに関連するすべてのものを削除します。",
"text2": "続行するにはパスワードを入力してください。詳しい案内の記載したメールを送信します。",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "アカウントの削除",
"requestSuccess": "リクエストは成功しました。詳しい案内はメールでお知らせします。",
"passwordRequired": "パスワードを入力してください。",
@ -152,6 +185,7 @@
"scheduled": "{date} ({dateSince}) にVikunjaアカウントは削除されます。",
"scheduledCancel": "アカウントの削除を取り消す場合はこちらをご覧ください。",
"scheduledCancelText": "アカウントの削除を取り消すにはパスワードを入力してください:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "アカウント削除の取り消し",
"scheduledCancelSuccess": "アカウント削除は行われません。"
},
@ -171,7 +205,7 @@
"title": "プロジェクト名",
"color": "色",
"projects": "プロジェクト",
"parent": "Parent Project",
"parent": "親プロジェクト",
"search": "プロジェクトのキーワードを入力…",
"searchSelect": "クリックするかEnterキーを押してプロジェクトを選択",
"shared": "共有プロジェクト",
@ -194,8 +228,8 @@
},
"background": {
"title": "プロジェクトの背景画像を設定",
"remove": "背景画像削除",
"upload": "画像の選択",
"remove": "背景画像削除",
"upload": "画像を選択…",
"searchPlaceholder": "背景画像を検索…",
"poweredByUnsplash": "Powered by Unsplash",
"loadMore": "写真をもっと読み込む",
@ -225,7 +259,7 @@
"identifier": "プロジェクトID",
"identifierPlaceholder": "プロジェクトIDを入力…",
"description": "説明",
"descriptionPlaceholder": "プロジェクトの説明を入力…",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"color": "色",
"success": "プロジェクトは正常に更新されました。"
},
@ -254,9 +288,9 @@
"userTeam": {
"typeUser": "ユーザー",
"typeTeam": "チーム",
"shared": "Shared with these {type}",
"you": "You",
"notShared": "Not shared with any {type} yet.",
"shared": "アクセスできる{type}",
"you": "あなた",
"notShared": "まだどの{type}とも共有していません。",
"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}.",
@ -301,18 +335,21 @@
"title": "カンバン",
"limit": "Limit: {limit}",
"noLimit": "未設定",
"doneBucket": "Done bucket",
"doneBucket": "バケットを完了",
"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": "デフォルトのバケット",
"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": "タスクの追加",
"addAnotherTask": "他のタスクを追加",
"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?",
"addBucket": "新しいバケットの作成",
"addBucketPlaceholder": "新しいバケット名を入力…",
"deleteHeaderBucket": "バケットの削除",
"deleteBucketText1": "このバケットを削除して本当によろしいですか?",
"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.",
@ -323,41 +360,56 @@
"favorites": {
"title": "お気に入り"
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
}
},
"filters": {
"title": "絞り込み",
"clear": "絞り込みの解除",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
"title": "絞り込み条件名",
"titlePlaceholder": "絞り込み条件名を入力…",
"description": "説明",
"descriptionPlaceholder": "The description goes here…",
"includeNulls": "Include Tasks which don't have a value set",
"requireAll": "Require all filters to be true for a task to show up",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"includeNulls": "値が設定されていないタスクを含める",
"requireAll": "すべての条件に一致するタスクのみ表示",
"showDoneTasks": "完了したタスクを表示",
"sortAlphabetically": "アルファベット順に並べ替える",
"enablePriority": "優先度によるフィルターの有効化",
"enablePercentDone": "進捗状況によるフィルターの有効化",
"enablePriority": "優先度による絞り込みを有効化",
"enablePercentDone": "進捗状況による絞り込みを有効化",
"dueDateRange": "期日の範囲",
"startDateRange": "開始日の範囲",
"endDateRange": "開始日の範囲",
"reminderRange": "リマインダーの範囲"
},
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
"title": "新しい絞り込み条件の作成",
"description": "絞り込み条件は、複数の条件を組み合わせて保存できる仮想のプロジェクトです。",
"action": "新しい絞り込み条件を作成",
"titleRequired": "絞り込み条件名を入力してください。"
},
"delete": {
"header": "Delete this saved filter",
"text": "Are you sure you want to delete this saved filter?",
"success": "The filter was deleted successfully."
"header": "絞り込み条件の削除",
"text": "絞り込み条件を削除して本当によろしいですか?",
"success": "絞り込み条件は正常に削除されました。"
},
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
"title": "絞り込み条件の編集",
"success": "絞り込み条件は正常に保存されました。"
}
},
"migrate": {
@ -373,7 +425,9 @@
"alreadyMigrated2": "再びインポートすることも可能ですが、重複する可能性があります。インポートして本当によろしいですか?",
"confirm": "了解!インポート開始なのだ!",
"importUpload": "{name}からVikunjaにデータをインポートするには、以下のボタンをクリックしてファイルを選択してください。",
"upload": "ファイルのアップロード"
"upload": "ファイルのアップロード",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
"title": "ラベル",
@ -444,6 +498,7 @@
"custom": "カスタム",
"id": "ID",
"created": "Created at",
"createdBy": "Created by {0}",
"actions": "Actions",
"cannotBeUndone": "この操作は元に戻せません!"
},
@ -462,24 +517,59 @@
"edit": "編集",
"done": "完了",
"heading1": "見出し1",
"heading1Tooltip": "Big section heading.",
"heading2": "見出し2",
"heading2Tooltip": "Medium section heading.",
"heading3": "見出し3",
"heading3Tooltip": "Smaller section header.",
"headingSmaller": "見出しを小さくする",
"headingBigger": "見出しを大きくする",
"bold": "太字",
"italic": "斜体",
"strikethrough": "打ち消し線",
"underline": "Underline",
"code": "コード",
"codeTooltip": "Capture a code snippet.",
"quote": "引用",
"unorderedList": "順序なしリスト",
"orderedList": "順序付きリスト",
"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": "Clean Block",
"link": "リンク",
"image": "画像",
"table": "テーブル",
"imageTooltip": "Upload an image from your computer.",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
},
"horizontalRule": "横罫",
"horizontalRuleTooltip": "Divide a section.",
"sideBySide": "Side By Side",
"guide": "説明書"
"guide": "説明書",
"text": "Text",
"textTooltip": "Just start typing with plain text.",
"taskList": "Task list",
"taskListTooltip": "Track tasks with a to-do list.",
"undo": "Undo",
"redo": "Redo",
"placeholder": "Type some text or hit '/' to see more options…"
},
"multiselect": {
"createPlaceholder": "新規作成",
@ -492,17 +582,17 @@
"ranges": {
"today": "今日",
"thisWeek": "今週",
"restOfThisWeek": "現在日時から週末まで",
"restOfThisWeek": "から週末まで",
"nextWeek": "来週",
"next7Days": "7日間",
"lastWeek": "先週",
"thisMonth": "今月",
"restOfThisMonth": "現在日時から月末まで",
"restOfThisMonth": "から月末まで",
"nextMonth": "来月",
"next30Days": "30日間",
"lastMonth": "先月",
"thisYear": "今年",
"restOfThisYear": "現在日時から年末まで"
"restOfThisYear": "から年末まで"
}
},
"datemathHelp": {
@ -573,6 +663,9 @@
"belongsToProject": "このタスクはプロジェクト「{project}」に含まれています。",
"due": "Due {at}",
"closePopup": "閉じる",
"organization": "Organization",
"management": "Management",
"dateAndTime": "日付と時刻",
"delete": {
"header": "タスクの削除",
"text1": "このタスクを削除して本当によろしいですか?",
@ -617,17 +710,17 @@
"updated": "Updated"
},
"subscription": {
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribedTaskThroughParentProject": "現在プロジェクトを通して購読しているため、ここでこのタスクの購読を解除することはできません。",
"subscribedProject": "現在このプロジェクトを購読しており、変更通知が届きます。",
"notSubscribedProject": "このプロジェクトを購読していないため、変更通知は届きません。",
"subscribedTask": "現在このタスクを購読しており、変更通知が届きます。",
"notSubscribedTask": "このタスクを購読していないため、変更通知は届きません。",
"subscribe": "購読",
"unsubscribe": "購読解除",
"subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
"subscribeSuccessProject": "プロジェクトを購読しています",
"unsubscribeSuccessProject": "プロジェクトの購読を解除しました",
"subscribeSuccessTask": "このタスクを購読しています",
"unsubscribeSuccessTask": "このタスクの購読を解除しました"
},
"attachment": {
"title": "添付ファイル",
@ -650,7 +743,7 @@
"loading": "コメントを読み込み中…",
"edited": "edited {date}",
"creating": "コメントを作成中…",
"placeholder": "コメントを追加…",
"placeholder": "Add your comment, hit '/' for more options…",
"comment": "コメント",
"delete": "コメントの削除",
"deleteText1": "このコメントを削除して本当によろしいですか?",
@ -664,7 +757,7 @@
"1week": "1週間後"
},
"description": {
"placeholder": "説明を入力…",
"placeholder": "Enter a description, hit '/' for more options…",
"empty": "説明文はありません。"
},
"assignee": {
@ -733,7 +826,7 @@
"repeat": {
"everyDay": "毎日",
"everyWeek": "毎週",
"everyMonth": "毎月",
"every30d": "30日ごと",
"mode": "繰り返しモード",
"monthly": "毎月",
"fromCurrentDate": "現在時刻からの間隔",
@ -815,7 +908,7 @@
"namePlaceholder": "The team's name goes here…",
"nameRequired": "Please specify a name.",
"description": "説明",
"descriptionPlaceholder": "The teams description goes here…",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "管理者",
"member": "メンバー"
}
@ -826,34 +919,35 @@
"allPages": "これらのショートカットはすべてのページで機能します。",
"currentPageOnly": "これらのショートカットはこのページで機能します。",
"somePagesOnly": "これらのショートカットは一部のページでのみ機能します。",
"toggleMenu": "Toggle The Menu",
"toggleMenu": "メニューの表示/非表示",
"quickSearch": "Open the search/quick action bar",
"then": "then",
"then": " ",
"task": {
"title": "Task Page",
"title": "タスク",
"done": "タスクの完了/取り消し",
"assign": "Assign this task to a user",
"labels": "Add labels to this task",
"dueDate": "Change the due date of this task",
"attachment": "このタスクに添付ファイルを追加",
"related": "Modify related tasks of this task",
"color": "タスクの色の変更",
"move": "Move this task to another project",
"reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description",
"delete": "タスクの削除",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
"assign": "タスクに担当者を割り当てる",
"labels": "タスクにラベルを追加",
"dueDate": "タスクの期日を設定",
"attachment": "タスクに添付ファイルを追加",
"related": "関連タスクを追加",
"color": "タスクの色を変更",
"move": "タスクを別のプロジェクトに移動",
"reminder": "タスクのリマインダーを設定",
"description": "タスクの説明を編集",
"delete": "タスクを削除",
"priority": "タスクの優先度を設定",
"favorite": "タスクをお気に入りに追加/削除",
"save": "Save the current task"
},
"project": {
"title": "Project Views",
"title": "プロジェクト",
"switchToListView": "リストで表示",
"switchToGanttView": "ガントチャートで表示",
"switchToKanbanView": "カンバンボードで表示",
"switchToTableView": "テーブルで表示"
},
"navigation": {
"title": "Navigation",
"title": "ナビゲーション",
"overview": "概要に移動",
"upcoming": "今後の予定に移動",
"labels": "ラベルに移動",
@ -868,20 +962,20 @@
"menu": {
"edit": "編集",
"archive": "アーカイブ",
"duplicate": "Duplicate",
"duplicate": "複製",
"delete": "削除",
"unarchive": "アーカイブの取り消し",
"setBackground": "Set background",
"setBackground": "背景画像の設定",
"share": "共有",
"newProject": "新しいプロジェクトの作成",
"createProject": "Create project"
"createProject": "プロジェクトの作成"
},
"apiConfig": {
"url": "Vikunja URL",
"urlPlaceholder": "例: https://localhost:3456",
"change": "変更",
"use": "{0} に設置されたVikunjaを使用します。",
"error": "\"{domain}\" にはVikunjaは存在しないか使用できない状態です。別のURLでお試しください。",
"error": "\"{domain}\" にはVikunjaは存在しないか使用できない状態です。URLの形式が正しいかどうか、そして直接アクセスして到達きるかどうかを確認し、もう一度お試しください。",
"success": "\"{domain}\" に設置されたVikunjaを使用します。",
"urlRequired": "URLは必須です。"
},
@ -891,9 +985,11 @@
"contact": "contact us"
},
"notification": {
"title": "Notifications",
"none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
"title": "通知",
"none": "通知はありません。それではよい一日を!",
"explainer": "購読しているプロジェクトやタスクが変更されると通知されます。",
"markAllRead": "Mark all notifications as read",
"markAllReadSuccess": "Successfully marked all notifications as read."
},
"quickActions": {
"commands": "コマンド",
@ -902,11 +998,12 @@
"tasks": "タスク",
"projects": "プロジェクト",
"teams": "チーム",
"labels": "Labels",
"newProject": "新しいプロジェクト名を入力…",
"newTask": "新しいタスク名を入力…",
"newTeam": "新しいチーム名を入力…",
"createTask": "現在のプロジェクト ({title}) にタスクを作成",
"createProject": "Create a project",
"createProject": "プロジェクトを作成",
"cmds": {
"newTask": "新しいタスクの作成",
"newProject": "新しいプロジェクトの作成",
@ -983,7 +1080,7 @@
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
"10005": "There can be only one done bucket per project.",
"11001": "The saved filter does not exist.",
"11002": "Saved filters are not available for link shares.",
"11002": "絞り込み条件はリンクの共有には使用できません。",
"12001": "The subscription entity type is invalid.",
"12002": "You are already subscribed to the entity itself or a parent entity.",
"13001": "This link share requires a password for authentication, but none was provided.",

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