forked from vikunja/frontend
Compare commits
199 Commits
6083301d1f
...
66e60a4e6a
Author | SHA1 | Date |
---|---|---|
renovate | 66e60a4e6a | |
renovate | 0f67a78ec8 | |
renovate | 722802fb2e | |
renovate | 752ead3a75 | |
renovate | b0b261d647 | |
renovate | 442a14242c | |
Dominik Pschenitschni | 66c0c322a2 | |
Dominik Pschenitschni | f4bc2b94f0 | |
Dominik Pschenitschni | f7728e5384 | |
kolaente | 78b765ddc4 | |
kolaente | f967bcb205 | |
Dominik Pschenitschni | e49f960aea | |
renovate | 98cb878250 | |
renovate | 03f2b253b8 | |
renovate | 69c0726b9d | |
Dominik Pschenitschni | eb59ca5836 | |
Dominik Pschenitschni | 8b7b4d61a3 | |
renovate | 0ed7114260 | |
renovate | dda162c16f | |
Dominik Pschenitschni | eeb562314e | |
Dominik Pschenitschni | 7f00c7dabd | |
konrad | 0ff0d8c5b8 | |
renovate | 9c9a5d08ff | |
renovate | 9a2b88d295 | |
kolaente | 09b76b7bd4 | |
renovate | f72c847e99 | |
renovate | 8ea899fa26 | |
Dominik Pschenitschni | e01df4d369 | |
Dominik Pschenitschni | 096daad80a | |
Dominik Pschenitschni | 3c5bfcc6f3 | |
Dominik Pschenitschni | 0182695cda | |
Dominik Pschenitschni | 44e6981759 | |
renovate | 15b64c7e8a | |
renovate | 9c357cb83e | |
renovate | 0cf9b7595a | |
renovate | 21dce0d8a8 | |
renovate | 218b96b230 | |
Dominik Pschenitschni | d19c48a4f5 | |
Dominik Pschenitschni | 480aa8813e | |
Dominik Pschenitschni | caa29c152d | |
Dominik Pschenitschni | 1101fcb3ff | |
Dominik Pschenitschni | 5d601ca4b3 | |
Dominik Pschenitschni | 53c9a9bc9c | |
Dominik Pschenitschni | d6cb965ea7 | |
Dominik Pschenitschni | 964aba4824 | |
Dominik Pschenitschni | 35f4bb1385 | |
Dominik Pschenitschni | 0b58973d87 | |
Dominik Pschenitschni | 02deb0bedd | |
Dominik Pschenitschni | 4cd0e90fea | |
Dominik Pschenitschni | e8c6afce72 | |
Dominik Pschenitschni | a2c1702eef | |
Dominik Pschenitschni | 599e28e5e5 | |
Dominik Pschenitschni | 1002579173 | |
Dominik Pschenitschni | 5ae8bace82 | |
Dominik Pschenitschni | 0832184222 | |
Dominik Pschenitschni | a50eca852f | |
Dominik Pschenitschni | b4f4fd45a4 | |
Dominik Pschenitschni | 15ef86d597 | |
Dominik Pschenitschni | 825ba100f0 | |
Dominik Pschenitschni | 839d331bf5 | |
renovate | 1798388e31 | |
renovate | c3f8dcefb6 | |
renovate | 816292e86a | |
renovate | ea1c7f1a7e | |
renovate | 6cb17c1267 | |
renovate | cbb2cf2951 | |
renovate | 85e1b36b00 | |
renovate | c9b9367c0b | |
renovate | a14644c156 | |
renovate | 189b5ee8aa | |
renovate | 61ed47fab4 | |
renovate | f18c03fa4d | |
renovate | 0e219b48a3 | |
renovate | 9ee05d5583 | |
renovate | 6d20e762ee | |
drone | 5143e09d2b | |
drone | b428523c89 | |
renovate | 6ef0a0ded9 | |
kolaente | bd7fc44722 | |
kolaente | 549e7b4310 | |
renovate | 89a125599e | |
kolaente | da2a7a224e | |
drone | da478a49d1 | |
kolaente | 98943377b8 | |
renovate | d28bbb7dc0 | |
Dominik Pschenitschni | 386fd79b49 | |
Dominik Pschenitschni | 9807858436 | |
Dominik Pschenitschni | 9ded3d0cd6 | |
konrad | d5258b7315 | |
renovate | eccaeae9e9 | |
konrad | fd3e7e655d | |
kolaente | 5271166120 | |
Dominik Pschenitschni | 61a89117d2 | |
Dominik Pschenitschni | 066553838a | |
Dominik Pschenitschni | 443e1a063d | |
Dominik Pschenitschni | 9a84fb6d7f | |
Dominik Pschenitschni | d8d3e4c8a6 | |
Dominik Pschenitschni | b4f88bd4a6 | |
Dominik Pschenitschni | abc26496cf | |
Dominik Pschenitschni | b8cc828bc0 | |
Dominik Pschenitschni | 874dc1e5fc | |
Dominik Pschenitschni | e74e6fcc99 | |
Dominik Pschenitschni | 52d4d0bdb9 | |
Dominik Pschenitschni | 6bf6357cbd | |
Dominik Pschenitschni | cf0eaf9ba1 | |
Dominik Pschenitschni | 8dea4082bb | |
Dominik Pschenitschni | 51dc123d89 | |
Dominik Pschenitschni | acb3ddc73f | |
Dominik Pschenitschni | 407f5f2ef8 | |
Dominik Pschenitschni | 73eab6c5b5 | |
Dominik Pschenitschni | aefda38bdd | |
Dominik Pschenitschni | a70a2e3ba6 | |
Dominik Pschenitschni | db611ab2d3 | |
kolaente | e1f49f2ff1 | |
kolaente | b8e7b87f96 | |
kolaente | 6c619072b4 | |
kolaente | 26e522cf8c | |
Dominik Pschenitschni | 7f4114b703 | |
Dominik Pschenitschni | c7dd20ef57 | |
Dominik Pschenitschni | c1da04eda1 | |
Dominik Pschenitschni | 2c732eb0d5 | |
Dominik Pschenitschni | 2acb70c562 | |
Dominik Pschenitschni | eaf777864a | |
Dominik Pschenitschni | 0b194bb0cf | |
Dominik Pschenitschni | e968c88cfd | |
Dominik Pschenitschni | df02dd5291 | |
Dominik Pschenitschni | acdbf2f8f5 | |
Dominik Pschenitschni | 9f146c8c7f | |
Dominik Pschenitschni | 3b244dfdbe | |
Dominik Pschenitschni | 2f820e517f | |
kolaente | 56b88218b3 | |
kolaente | 957d8f05a5 | |
kolaente | 31f2065d20 | |
kolaente | f5fd14124f | |
Dominik Pschenitschni | d91bc5090a | |
Dominik Pschenitschni | f21a4e1e9f | |
kolaente | 970a04d973 | |
kolaente | fd9d0ad155 | |
kolaente | 4be0977014 | |
kolaente | 6975a2b286 | |
kolaente | 64fdae81ec | |
kolaente | 56a25734d7 | |
kolaente | ed5d3be7cb | |
kolaente | 98d0398ca8 | |
kolaente | d3925b8d80 | |
kolaente | b7b4530a11 | |
kolaente | 766b4c669f | |
kolaente | 5f7159ebc4 | |
kolaente | 0a9588e097 | |
kolaente | 091beecc19 | |
kolaente | 6cb331ee0f | |
kolaente | 8c62a9e198 | |
kolaente | 29dcc02217 | |
kolaente | 3eacc0754f | |
kolaente | ebd824bddf | |
kolaente | 2c012e1a08 | |
kolaente | 10c6db3849 | |
kolaente | 80c151ca6c | |
kolaente | 7a7a1c985e | |
kolaente | c8eac914d1 | |
kolaente | d2c40926de | |
kolaente | c3cae78213 | |
kolaente | c289a6ae18 | |
kolaente | ef4689335b | |
kolaente | 3b48adad67 | |
kolaente | 736e5a8bf5 | |
kolaente | ed241d21be | |
kolaente | 49a24977f9 | |
kolaente | 2b0df8c237 | |
renovate | ef3f19d046 | |
Dominik Pschenitschni | 7ce880239e | |
Dominik Pschenitschni | aa2278a564 | |
renovate | 96e44bf225 | |
renovate | 4ad99bdad1 | |
renovate | 5e7fe3280c | |
renovate | 7ec31363c3 | |
drone | c40c1fb10a | |
renovate | 59be904d4a | |
drone | 1d9d093b31 | |
renovate | ef6bc3cbab | |
kolaente | e13e477682 | |
kolaente | 8a5b1ab3e3 | |
renovate | 70e81ee682 | |
kolaente | a0795db040 | |
renovate | 35649d0e87 | |
drone | 67145fe00b | |
renovate | 22d93a1a3c | |
kolaente | 51471b9551 | |
kolaente | 22a18f8437 | |
kolaente | f17bbeddec | |
kolaente | eae555475d | |
kolaente | 12faafbe7c | |
kolaente | 5ddce387fe | |
renovate | 05d000fc50 | |
renovate | 333df9b247 | |
renovate | 8d368c552d | |
renovate | 57cc7b8f37 | |
renovate | 527873dad4 | |
renovate | d67dca4a81 |
|
@ -202,8 +202,9 @@ steps:
|
|||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
commands:
|
||||
- apk add git
|
||||
- corepack enable && pnpm config set store-dir .cache/.pnp
|
||||
- pnpm install --fetch-timeout 100000
|
||||
- pnpm install --fetch-timeout 100000 --frozen-lockfile
|
||||
- pnpm run lint
|
||||
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
|
||||
- pnpm run build
|
||||
|
@ -278,8 +279,9 @@ steps:
|
|||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
commands:
|
||||
- apk add git
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm install --fetch-timeout 100000
|
||||
- pnpm install --fetch-timeout 100000 --frozen-lockfile
|
||||
- pnpm run lint
|
||||
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
|
||||
- pnpm run build
|
||||
|
@ -659,6 +661,6 @@ steps:
|
|||
from_secret: crowdin_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: c885a0e50db729842402494aa645dd3ac662828b691108550f6bf302158295ba
|
||||
hmac: 5dc7ab785b6e4d1611fc2851971e23c444d93d4988517f116e02e8c4d1af82f3
|
||||
|
||||
...
|
||||
|
|
|
@ -5,7 +5,7 @@ module.exports = {
|
|||
'root': true,
|
||||
'env': {
|
||||
'browser': true,
|
||||
'es2021': true,
|
||||
'es2022': true,
|
||||
'node': true,
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
|
@ -37,6 +37,10 @@ module.exports = {
|
|||
'@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
||||
|
||||
'vue/multi-word-component-names': 0,
|
||||
// disabled until we have support for reactivityTransform
|
||||
// See https://github.com/vuejs/eslint-plugin-vue/issues/1948
|
||||
// see also setting in `vite.config`
|
||||
'vue/no-setup-props-destructure': 0,
|
||||
},
|
||||
'parser': 'vue-eslint-parser',
|
||||
'parserOptions': {
|
||||
|
|
557
CHANGELOG.md
557
CHANGELOG.md
|
@ -9,6 +9,563 @@ All releases can be found on https://code.vikunja.io/frontend/releases.
|
|||
|
||||
The releases aim at the api versions which is why there are missing versions.
|
||||
|
||||
## [0.20.0] - 2022-10-28
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(filters)* Changing filter checkbox values not being emitted to parent components
|
||||
* *(filters)* Make sure all checkboxes are aligned properly
|
||||
* *(filters)* Page freezing when entering a date as a result of an endless loop
|
||||
* *(gantt)* Only unmount chart if there aren't any loaded tasks yet
|
||||
* *(gantt)* UseDayjsLanguageSync and move to separate file
|
||||
* *(i18n)* Spelling typo
|
||||
* *(i18n)* Rename "right" to permission so that it's clearer what it is used for
|
||||
* *(labels)* Unset loading state after loading all labels
|
||||
* *(lint)* Unnecessary catch clause
|
||||
* *(list)* Automatically close task edit pane when switching between lists
|
||||
* *(quick add magic)* Time parsing for certain conditions (#2367)
|
||||
* *(sharing)* Correctly check if the user has admin rights when sharing
|
||||
* *(subscription)* Don't remove every namespace but the one subscribing to
|
||||
* *(subscription)* Make sure list subscription state is propagated everywhere for the current list
|
||||
* *(task)* Make sure users can be assigned via quick add magic via their real name as well
|
||||
* *(task)* Cancel loading state when creating a new task does not work
|
||||
* *(task)* Cancel loading state when creating a new task does not work
|
||||
* *(task)* New tasks with quick add magic not showing up in task list
|
||||
* *(task)* Setting a priority was not properly saved
|
||||
* *(task)* Setting progress was not properly saved
|
||||
* *(task)* Setting a label would not show up on the kanban board after setting it
|
||||
* *(task)* Stop loading when no list was specified while creating a task
|
||||
* *(task)* Only show create list or import cta when there are no tasks
|
||||
* *(task)* Marking checklist items as done
|
||||
* *(task)* Focusing on assignee search field when activating it
|
||||
* *(task)* Scroll the task field into view after activating it
|
||||
* *(tasks)* Don't allow adding the same assignee multiple times
|
||||
* *(teams)* Show an error message when no user is selected to add to a team
|
||||
* *(tests)* Fake current time in gantt tests to make them more reliable
|
||||
* *(tests)* Adjust gantt rows identifier* Authenticate per request (#2258) ([6e4a3ff](6e4a3ff1996f55d99896a0e8267c1915de09dd39))
|
||||
* Add lodash.clonedeep types ([80eaf38](80eaf38090413b74524ddc5a7dfcc9a845a6ba26))
|
||||
* Use correct model for generics ([3ba423e](3ba423ed238a5f8f445246793829c7645dfe42aa))
|
||||
* Merge duplicate types ([106abfc](106abfc842ca0c916ef7574b0fe5c89940869ac2))
|
||||
* CreateNewTask typing ([f9b5130](f9b51306c396ceb0d8fa0c4af3fea24d2b28b64b))
|
||||
* Improve some types ([4a50e6a](4a50e6aae28d22c3d441f1fead4edce7d0e30ff1))
|
||||
* Use definite assignment assertion operator ([96f5f00](96f5f00c073f71c71d85c351f86ad16a67db6992))
|
||||
* Mark abstractModel and abstractService abstract ([d36577c](d36577c04e1eea00fb21a5fb774e7f6b1f667d54))
|
||||
* Use IAbstract to extend model interface ([8be1f81](8be1f81848303d590adb890743dd688fbf5cdf1c))
|
||||
* Use new assignData method for default data ([8416b1f](8416b1f44811ff477d81db20370ff68e899c7252))
|
||||
* Don't push a select event when nothing was selected ([9616bad](9616badc33173483e0b5cc0c99655e0c9a4907f9))
|
||||
* Don't try to set the bucket of a task when it was moved to a new list ([c06b781](c06b781837c66174be41f40c967fbfcbcc35495e))
|
||||
* Mutation error in TaskDetailView ([b4cba6f](b4cba6f7d96334b46e5e2d6be5ac87432b01f0c0))
|
||||
* DefaultListId ([878b5bf](878b5bf236f7d1ddc9825d8dca8415313b08fd94))
|
||||
* Use typed useStore ([54de368](54de368642519fc900ce89e4ee38989555054a05))
|
||||
* Don't encode attachment upload file blob as json ([d819b9b](d819b9b0ba08db24a77751061ae285fc11205c2c))
|
||||
* Dragging a list on mobile Safari ([6bf5f6e](6bf5f6efd46c47293fb54b9e9a25d91d8c6bec0d))
|
||||
* Introduce a ListView type to properly type all available list views ([23598dd](23598dd2ee649449f2176ae86acbc16ecbf01e6f))
|
||||
* Use proper computed for available views list ([e67fc7f](e67fc7fb7e1678b1b691fee77d3237b222ad50c6))
|
||||
* Only warn once if triggeredNotifications are not supported (#2344) ([f083f18](f083f181e2c8aa0af3ac1381303f61792d5975f5))
|
||||
* Bucket title edit success message appearing twice ([4921788](49217889b50da73d0f4851c4ee21f0dec11c7958))
|
||||
* Don't parse dates in urls ([92f24e5](92f24e59a794a25098f5fb50f2101d516829cd36))
|
||||
* Vue-i18n global scope (#2366) ([602ab83](602ab8379e3fb11eb8b547d036921311f193fb12))
|
||||
* Redirect to login when the jwt token expires ([91976e2](91976e23f989f39fb25d3341aa3f4b632ea66f35))
|
||||
* Only try to save user settings when a user is authenticated ([2df2bd3](2df2bd38e2b9f86be7e7c5aab744f27cbf2644c3))
|
||||
* Remove margin from the color bubble component itself ([4fce71f](4fce71f729878d47c3ec79d0c10fae8fbaabbd91))
|
||||
* Test pnpm cache ([e5d04c9](e5d04c98dabc6b597ecc32dd01ab31c4dd9882d1))
|
||||
* Remove console.log ([43e2d03](43e2d036d77731fcce18cbea1d82196b10016609))
|
||||
* Explicitly install cypress ([62e227c](62e227c767a43578f4487e3dc244f4756e073f5d))
|
||||
* Only pass date to flatpickr if it's a valid date ([ede5cdd](ede5cdd8cf5575bba96d3e7b6824a7ad7b414ea7))
|
||||
* Loading state when creating a new task from list view ([aa64e98](aa64e9835c6b9ef2bb10ab8d2a1b4a695cb4321b))
|
||||
* Make add task button 100% height ([3c9c5ef](3c9c5eff1258b6e04e3d0e9299110fa9b5c9757d))
|
||||
* Lint ([2bf9771](2bf9771e2894acb7ad3e563b7b31442d91c49e1a))
|
||||
* Color list titles so that they are visible on cards with a background ([62ed7c5](62ed7c5964f1252f09fe432c42aaf327da5a8c4f))
|
||||
* Missed porting these getters and commits ([95ad245](95ad245b59b0c6398b0bca217572ca36f6ea5a54))
|
||||
* Use https for api url (#2425) ([9f39365](9f3936544d5906f0031412139b53c286023c2405))
|
||||
* Don't use corepack prepare at all ([a199fc7](a199fc7a8e7f621ee96b2079e9558987f1350493))
|
||||
* Add types for node ([6a82807](6a828078a398ab920f0e29d0801b918ae092ef30))
|
||||
* VueI18n global scope fallback warnings (#2437) ([e9cf562](e9cf562969e42cc3ce3ffba3ed093db7a2089395))
|
||||
* Fix missed conversion to ref (#2445) ([94d6f38](94d6f38e89174f879be4e5b1897b52603b40a745))
|
||||
* Don't emit a possible null task ([5f5ed41](5f5ed410df1a2fe73e821d7dee7ebd4c0b918069))
|
||||
* Docker build ([5b60693](5b606936c3f7b0dc1232ad269f3666f8170c6e11))
|
||||
* Update top header list title when saving a filter ([fd3c15d](fd3c15d0642a8d91260ba24eaae52e0ba62c2871))
|
||||
* Type of config stores maxFileSize (#2455) ([78a6d38](78a6d38641c5e4e68f117e37ee36a4ca3b40a24b))
|
||||
* Don't add class method to interface ([367ad1e](367ad1e5a5972ac6ff353275b31f309ebcf5cb4c))
|
||||
* Attachment deletion (#2472) ([f1852f1](f1852f1f33401576ae5033f54613c96cd80e0f95))
|
||||
* Add lodash.debounce types (#2487) ([00e0a23](00e0a23d48c19c440aea7857c8b162a0dfa34361))
|
||||
* Initial modal scroll lock (#2489) ([eae7cc5](eae7cc5a6b506cbbbe694b831cba7c5d1febaf05))
|
||||
* Unset cover image when the task does not have one ([054d70c](054d70cbe5344e39d0e5f277a7db2f26573e1efa))
|
||||
* Lint ([43258ab](43258ab74e0733e91be3ade1f0b13dcf9342cc18))
|
||||
* Lint ([84a1abf](84a1abf3477abbbee136979bd0bde08ae6c54ceb))
|
||||
* Don't try to render auth routes when the user is not authenticated ([3af20b6](3af20b6220d8fcded9c8c2f0bdef21dc26d748f6))
|
||||
* Lint ([f405b21](f405b2105bf4d1cfd4f6acf03210b37ac91eff5e))
|
||||
* Make sure subscriptions are properly inherited between lists and namespaces ([a895bde](a895bde6612e7a2b22a84b6ca7c583bafc9ebc9e))
|
||||
* Make sure subscription strings work consistently across languages ([172d353](172d353df7a86baa9c2759907c7f855679138cc0))
|
||||
* Make sure subscriptions are properly inherited between namespaces and lists ([0a29197](0a29197715f22602faf353fb8fe850150aa710d1))
|
||||
* Lint ([c6d6da3](c6d6da31712906f094a88dbfdb5e9b6db66c29e3))
|
||||
* Move hourToDaytime to separate file in order to pass tests ([5afafb7](5afafb7c82837a3af58c7bdc18174a785691b885))
|
||||
* Postcss-preset-env configuration (#2554) ([b80f82c](b80f82c4118bb372263130df80d15a2a79d2191e))
|
||||
* Password reset ([7357530](73575302debbe095ce031e4871fb3797a801db18))
|
||||
* Email confirmation ([e6f7ddc](e6f7ddc9ce90ddcb3b58b2c001320b6b2c3ac169))
|
||||
* Lint ([643a5b6](643a5b6d7d00bfab4b338582c85217dffa7d9b22))
|
||||
* Make sure services without a modelFactory override still return data ([8fdd3e7](8fdd3e785d3c55281b557827860d0532b94ac758))
|
||||
* Make sure share modals don't have a create button ([ae27502](ae27502022469882656459b0a9e7e8a4b6972c58))
|
||||
* Redirect with query parameters ([f61723d](f61723dac251c9d85102beae73c6a03df10bd4bf))
|
||||
* Task detail view top spacing on mobile ([a695719](a6957191284a8da38e56b4ed3fe0a57b69d6e2b9))
|
||||
* Make sure the filter button is always shown on the kanban board ([8023006](80230069c6f09ced484cd356b816df6b1dd296d6))
|
||||
* Wait until everything is loaded before replacing the current view with the last or login view ([6083301](6083301d1f410ede5fe62127e484169d74ff6dc0))
|
||||
* Show frontend version in about dialog ([5ddce38](5ddce387fe589c574adf0cce438732faf4ad9fd1))
|
||||
* Building version into releases ([a0795db](a0795db0408b5fece13d8a74e9e243375883ca6f))
|
||||
* Lint ([e13e477](e13e477682ef9fd647925f459d8d4527d3c55b9b))
|
||||
* New task input styling ([c3cae78](c3cae78213b791c9e6fd8143ee59e3ca256c374a))
|
||||
* Handle bar styling so they can actually be used ([10c6db3](10c6db3849e734d0508c8d435164a0f771175740))
|
||||
* Make sure the date format is actually valid ([2c012e1](2c012e1a080bd9519384d65ee0653483aa52d1c3))
|
||||
* Make tests work again with new selectors ([091beec](091beecc19cf5ff49fc252c4eeb98aa8a65ddb67))
|
||||
* Use inherit for font family ([b7b4530](b7b4530a111d93e81fc6398dc3f7267cc6e255fb))
|
||||
* Remove precision setting ([970a04d](970a04d9733f4cbdc35e5b772ce4a34fa71e6c4c))
|
||||
* Fix imports ([d91bc50](d91bc5090a6cec38e655c944df7cf57ac16e4133))
|
||||
* Use base store ([f5fd141](f5fd14124fa139f3e76f7a4915b2efc85de6c789))
|
||||
* Correctly import all components ([31f2065](31f2065d2005b27ff8a0abbc4efaa7138cfe27c1))
|
||||
* Update eslint env to 2022 ([0b194bb](0b194bb0cf326104c249c953194997a1f9a80dbf))
|
||||
* Don't try to dynamically load dayjs locales ([b8e7b87](b8e7b87f96bdccf19066ce31d40cf40379014bbe))
|
||||
* Disable dayjsLanguageSync function ([e1f49f2](e1f49f2ff15286ee8903c29dbe708cda90e5d70d))
|
||||
* Scope ListGantt styles ([73eab6c](73eab6c5b5bfe0d72393ab378cce77ad5cbb59b6))
|
||||
* Initial transformation of ganttBars ([407f5f2](407f5f2ef8c4759ea46f5fb74717bafb16f606c5))
|
||||
* ParseBooleanProp ([8dea408](8dea4082bb0766297f74acef0352f8a6a6168d3c))
|
||||
* Do not change language to the current one ([abc2649](abc26496cf0e20d0124af327d47e086b39e2bd23))
|
||||
* Remove IE fallback ([b4f88bd](b4f88bd4a6ba50be1f972794c3e87b7a09f7c2ca))
|
||||
* Improve return type ([0665538](066553838ad289d6c6c0a8b1c6ed0b84139ace54))
|
||||
* Improve notifications (#2583) ([9ded3d0](9ded3d0cd69dd974ffea2531e3ca92438e420f29))
|
||||
* Lint ([9894337](98943377b8344f1f5a8e38c23eff79d7678f51bc))
|
||||
* Label multiselect styling on focus ([da2a7a2](da2a7a224e3c8015939e189692813bc215dbd72c))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.11.0 (#2274)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.11.1 (#2275)
|
||||
* *(deps)* Update dependency vitest to v0.22.1 (#2276)
|
||||
* *(deps)* Update dependency sass to v1.54.8 (#2281)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001387 (#2285)
|
||||
* *(deps)* Update dependency rollup to v2.79.0 (#2278)
|
||||
* *(deps)* Update dependency marked to v4.1.0 (#2284)
|
||||
* *(deps)* Update dependency netlify-cli to v11 (#2287)
|
||||
* *(deps)* Update dependency vite to v3.0.9 (#2279)
|
||||
* *(deps)* Update dependency date-fns to v2.29.2 (#2277)
|
||||
* *(deps)* Update dependency esbuild to v0.15.6 (#2290)
|
||||
* *(deps)* Update dependency vite-plugin-pwa to v0.12.4 (#2291)
|
||||
* *(deps)* Update dependency rollup-plugin-visualizer to v5.8.0 (#2282)
|
||||
* *(deps)* Update dependency easymde to v2.17.0 (#2283)
|
||||
* *(deps)* Update dependency vue-tsc to v0.40.5 (#2292)
|
||||
* *(deps)* Update dependency vue to v3.2.38 (#2293)
|
||||
* *(deps)* Update dependency vue-router to v4.1.5 (#2294)
|
||||
* *(deps)* Update vueuse to v9.1.1 (#2295)
|
||||
* *(deps)* Update dependency @cypress/vue to v4.2.0 (#2296)
|
||||
* *(deps)* Update dependency @faker-js/faker to v7.5.0 (#2297)
|
||||
* *(deps)* Update dependency eslint to v8.23.0 (#2299)
|
||||
* *(deps)* Update dependency cypress to v10.7.0 (#2298)
|
||||
* *(deps)* Update dependency eslint-plugin-vue to v9.4.0 (#2300)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.12.0 (#2307)
|
||||
* *(deps)* Update dependency dompurify to v2.4.0 (#2306)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.36.1 (#2304)
|
||||
* *(deps)* Update dependency vite-svg-loader to v3.5.1 (#2302)
|
||||
* *(deps)* Update dependency typescript to v4.8.2 (#2301)
|
||||
* *(deps)* Update font awesome to v6.2.0 (#2303)
|
||||
* *(deps)* Update dependency @kyvg/vue3-notification to v2.4.1 (#2305)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.12.1 (#2308)
|
||||
* *(deps)* Update dependency vite-plugin-pwa to v0.12.6 (#2309)
|
||||
* *(deps)* Update dependency vue-tsc to v0.40.6 (#2310)
|
||||
* *(deps)* Update dependency rollup-plugin-visualizer to v5.8.1 (#2311)
|
||||
* *(deps)* Update dependency vitest to v0.23.0 (#2312)
|
||||
* *(deps)* Update dependency esbuild to v0.15.7 (#2313)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001390 (#2314)
|
||||
* *(deps)* Update dependency vue-tsc to v0.40.7 (#2315)
|
||||
* *(deps)* Update dependency vitest to v0.23.1 (#2316)
|
||||
* *(deps)* Update dependency @vitejs/plugin-legacy to v2.1.0 (#2317)
|
||||
* *(deps)* Update dependency @vitejs/plugin-vue to v3.1.0 (#2318)
|
||||
* *(deps)* Update dependency vite to v3.1.0 (#2319)
|
||||
* *(deps)* Update vueuse to v9.2.0 (#2320)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.36.2 (#2321)
|
||||
* *(deps)* Update dependency vue-tsc to v0.40.9 (#2322)
|
||||
* *(deps)* Pin dependency @types/lodash.clonedeep to 4.5.7 (#2323)
|
||||
* *(deps)* Update dependency @vue/eslint-config-typescript to v11.0.1 (#2324)
|
||||
* *(deps)* Update dependency vite-plugin-pwa to v0.12.7 (#2325)
|
||||
* *(deps)* Update dependency vue-tsc to v0.40.10 (#2326)
|
||||
* *(deps)* Update dependency postcss-preset-env to v7.8.1 (#2328)
|
||||
* *(deps)* Update dependency vite-svg-loader to v3.6.0 (#2327)
|
||||
* *(deps)* Update dependency vue-tsc to v0.40.11 (#2333)
|
||||
* *(deps)* Update dependency sass to v1.54.9 (#2336)
|
||||
* *(deps)* Update dependency vue-tsc to v0.40.13
|
||||
* *(deps)* Update dependency vue to v3.2.39
|
||||
* *(deps)* Update dependency typescript to v4.8.3 (#2341)
|
||||
* *(deps)* Update dependency vitest to v0.23.2
|
||||
* *(deps)* Update dependency autoprefixer to v10.4.9
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001397
|
||||
* *(deps)* Update dependency netlify-cli to v11.7.1
|
||||
* *(deps)* Update dependency eslint to v8.23.1
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.37.0
|
||||
* *(deps)* Update dependency blurhash to v2 (#2351)
|
||||
* *(deps)* Update dependency date-fns to v2.29.3 (#2354)
|
||||
* *(deps)* Update dependency autoprefixer to v10.4.10 (#2355)
|
||||
* *(deps)* Update dependency cypress to v10.8.0 (#2359)
|
||||
* *(deps)* Update dependency autoprefixer to v10.4.11 (#2363)
|
||||
* *(deps)* Update dependency postcss-preset-env to v7.8.2
|
||||
* *(deps)* Update dependency vite to v3.1.1 (#2365)
|
||||
* *(deps)* Pin dependency @types/dompurify to 2.3.4
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.13.0
|
||||
* *(deps)* Update dependency eslint-plugin-vue to v9.5.0 (#2371)
|
||||
* *(deps)* Update dependency eslint-plugin-vue to v9.5.1 (#2373)
|
||||
* *(deps)* Update dependency vite to v3.1.2
|
||||
* *(deps)* Update dependency @types/sortablejs to v1.15.0
|
||||
* *(deps)* Update dependency vitest to v0.23.4
|
||||
* *(deps)* Update dependency esbuild to v0.15.8
|
||||
* *(deps)* Update dependency vite-plugin-pwa to v0.12.8 (#2375)
|
||||
* *(deps)* Update caniuse-and-related to v4.21.4 (#2379)
|
||||
* *(deps)* Update dependency netlify-cli to v11.8.0 (#2380)
|
||||
* *(deps)* Update dependency @vitejs/plugin-legacy to v2.2.0 (#2381)
|
||||
* *(deps)* Update dependency vite to v3.1.3 (#2382)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.38.0 (#2383)
|
||||
* *(deps)* Update dependency vite-plugin-pwa to v0.13.0 (#2385)
|
||||
* *(deps)* Update dependency easymde to v2.18.0 (#2386)
|
||||
* *(deps)* Update dependency autoprefixer to v10.4.12
|
||||
* *(deps)* Update dependency pinia to v2.0.22 (#2400)
|
||||
* *(deps)* Update dependency @vue/eslint-config-typescript to v11.0.2
|
||||
* *(deps)* Update dependency vite-plugin-pwa to v0.13.1
|
||||
* *(deps)* Update dependency rollup to v2.79.1
|
||||
* *(deps)* Update dependency codemirror to v5.65.9
|
||||
* *(deps)* Update pnpm to v7.12.1
|
||||
* *(deps)* Update dependency sass to v1.55.0
|
||||
* *(deps)* Update dependency esbuild to v0.15.9
|
||||
* *(deps)* Update pnpm to v7.12.2 (#2408)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001412 (#2421)
|
||||
* *(deps)* Update dependency netlify-cli to v11.8.3 (#2422)
|
||||
* *(deps)* Update dependency eslint to v8.24.0 (#2410)
|
||||
* *(deps)* Update vueuse to v9.3.0 (#2423)
|
||||
* *(deps)* Update dependency rollup-plugin-visualizer to v5.8.2 (#2420)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.38.1 (#2426)
|
||||
* *(deps)* Update dependency blurhash to v2.0.1
|
||||
* *(deps)* Update dependency cypress to v10.9.0 (#2429)
|
||||
* *(deps)* Update dependency @types/node to v16.11.62 (#2430)
|
||||
* *(deps)* Update dependency typescript to v4.8.4
|
||||
* *(deps)* Update dependency vue to v3.2.40
|
||||
* *(deps)* Update dependency blurhash to v2.0.2
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.14.0 (#2440)
|
||||
* *(deps)* Update dependency vite to v3.1.4 (#2439)
|
||||
* *(deps)* Update dependency @vue/test-utils to v2.1.0
|
||||
* *(deps)* Update dependency esbuild to v0.15.10
|
||||
* *(deps)* Update dependency @cypress/vite-dev-server to v3.2.0 (#2448)
|
||||
* *(deps)* Update dependency postcss to v8.4.17 (#2449)
|
||||
* *(deps)* Update dependency marked to v4.1.1
|
||||
* *(deps)* Update dependency @vitejs/plugin-vue to v3.1.2 (#2461)
|
||||
* *(deps)* Update dependency @types/node to v16.11.63 (#2464)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001414 (#2465)
|
||||
* *(deps)* Update pnpm to v7.13.0 (#2467)
|
||||
* *(deps)* Update dependency netlify-cli to v12 (#2466)
|
||||
* *(deps)* Update dependency vue-advanced-cropper to v2.8.5 (#2469)
|
||||
* *(deps)* Update dependency blurhash to v2.0.3 (#2468)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.14.1 (#2471)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.39.0
|
||||
* *(deps)* Update dependency @types/node to v16.11.64 (#2479)
|
||||
* *(deps)* Update dependency eslint-plugin-vue to v9.6.0 (#2480)
|
||||
* *(deps)* Update pnpm to v7.13.1
|
||||
* *(deps)* Update dependency vue-advanced-cropper to v2.8.6 (#2483)
|
||||
* *(deps)* Pin dependency @rushstack/eslint-patch to 1.2.0 (#2486)
|
||||
* *(deps)* Pin dependency @types/lodash.debounce to 4.0.7 (#2488)
|
||||
* *(deps)* Update dependency happy-dom to v7 (#2492)
|
||||
* *(deps)* Update dependency vite to v3.1.5
|
||||
* *(deps)* Update dependency happy-dom to v7.0.2
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.14.2
|
||||
* *(deps)* Update pnpm to v7.13.2
|
||||
* *(deps)* Update dependency vue-flatpickr-component to v9.0.8 (#2494)
|
||||
* *(deps)* Update dependency vite to v3.1.6
|
||||
* *(deps)* Update dependency happy-dom to v7.0.4 (#2499)
|
||||
* *(deps)* Update dependency @cypress/vite-dev-server to v3.3.0 (#2501)
|
||||
* *(deps)* Update dependency happy-dom to v7.0.6 (#2500)
|
||||
* *(deps)* Update dependency happy-dom to v7.3.0 (#2502)
|
||||
* *(deps)* Update dependency vitest to v0.24.0 (#2503)
|
||||
* *(deps)* Update dependency vue-tsc to v1 (#2504)
|
||||
* *(deps)* Update dependency happy-dom to v7.4.0 (#2505)
|
||||
* *(deps)* Update dependency eslint to v8.25.0
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.1 (#2507)
|
||||
* *(deps)* Update dependency pinia to v2.0.23 (#2509)
|
||||
* *(deps)* Update dependency express to v4.18.2
|
||||
* *(deps)* Update pnpm to v7.13.3 (#2511)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.2 (#2510)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.3 (#2512)
|
||||
* *(deps)* Update dependency netlify-cli to v12.0.7 (#2514)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001418 (#2513)
|
||||
* *(deps)* Update dependency vite to v3.1.7 (#2515)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.15.0 (#2516)
|
||||
* *(deps)* Update dependency vitest to v0.24.1 (#2517)
|
||||
* *(deps)* Update pnpm to v7.13.4 (#2518)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.40.0 (#2519)
|
||||
* *(deps)* Update dependency @types/node to v16.11.65 (#2520)
|
||||
* *(deps)* Update dependency minimist to v1.2.7 (#2521)
|
||||
* *(deps)* Update dependency rollup to v3 (#2524)
|
||||
* *(deps)* Update dependency @cypress/vite-dev-server to v3.3.1 (#2523)
|
||||
* *(deps)* Update dependency cypress to v10.10.0 (#2525)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.4 (#2526)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.5 (#2527)
|
||||
* *(deps)* Update dependency rollup to v3.1.0 (#2528)
|
||||
* *(deps)* Update dependency @faker-js/faker to v7.6.0 (#2530)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.6 (#2529)
|
||||
* *(deps)* Update dependency postcss to v8.4.18 (#2532)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.7 (#2533)
|
||||
* *(deps)* Update dependency vite to v3.1.8 (#2534)
|
||||
* *(deps)* Update dependency vue to v3.2.41 (#2538)
|
||||
* *(deps)* Update dependency vitest to v0.24.3 (#2536)
|
||||
* *(deps)* Update dependency @cypress/vue to v4.2.1 (#2535)
|
||||
* *(deps)* Update dependency esbuild to v0.15.11 (#2539)
|
||||
* *(deps)* Update dependency rollup to v3.2.0 (#2541)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.8 (#2540)
|
||||
* *(deps)* Update dependency rollup to v3.2.1 (#2545)
|
||||
* *(deps)* Update dependency @types/node to v16.11.66 (#2544)
|
||||
* *(deps)* Update dependency ufo to v0.8.6 (#2542)
|
||||
* *(deps)* Update dependency rollup-plugin-visualizer to v5.8.3 (#2543)
|
||||
* *(deps)* Update pnpm to v7.13.5
|
||||
* *(deps)* Update dependency rollup to v3.2.2 (#2549)
|
||||
* *(deps)* Update dependency netlify-cli to v12.0.9 (#2551)
|
||||
* *(deps)* Update vueuse to v9.3.1 (#2552)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001420 (#2550)
|
||||
* *(deps)* Update dependency happy-dom to v7.5.12 (#2553)
|
||||
* *(deps)* Pin dependency @types/postcss-preset-env to 7.7.0 (#2555)
|
||||
* *(deps)* Update dependency rollup to v3.2.3 (#2556)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.40.1 (#2557)
|
||||
* *(deps)* Update dependency @types/node to v16.11.68 (#2558)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.16.0 (#2560)
|
||||
* *(deps)* Update dependency esbuild to v0.15.12 (#2561)
|
||||
* *(deps)* Update pnpm to v7.13.6 (#2562)
|
||||
* *(deps)* Update dependency vue-flatpickr-component to v10 (#2563)
|
||||
* *(deps)* Update dependency eslint to v8.26.0 (#2564)
|
||||
* *(deps)* Update pnpm to v7.14.0 (#2565)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.9 (#2566)
|
||||
* *(deps)* Update dependency @types/node to v16.18.0 (#2567)
|
||||
* *(deps)* Update dependency happy-dom to v7.6.0 (#2571)
|
||||
* *(deps)* Update dependency @vue/test-utils to v2.2.0 (#2570)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001423 (#2568)
|
||||
* *(deps)* Update dependency netlify-cli to v12.0.11 (#2569)
|
||||
* *(deps)* Update dependency vue-router to v4.1.6 (#2572)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.41.0 (#2573)
|
||||
* *(deps)* Update dependency @types/node to v18 (#2574)
|
||||
* *(deps)* Update vueuse to v9.4.0 (#2575)
|
||||
* *(deps)* Update dependency cypress to v10.11.0 (#2576)
|
||||
* *(deps)* Update dependency @types/node to v18.11.6
|
||||
* *(deps)* Update dependency vite to v3.2.0 (#2580)
|
||||
* *(deps)* Update dependency @types/node to v18.11.7 (#2581)
|
||||
* *(deps)* Update dependency @vitejs/plugin-legacy to v2.3.0 (#2578)
|
||||
* *(deps)* Update dependency @vitejs/plugin-vue to v3.2.0 (#2579)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.17.0
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.17.1 (#2585)
|
||||
* *(deps)* Update dependency autoprefixer to v10.4.13 (#2586)
|
||||
|
||||
### Features
|
||||
|
||||
* *(gantt)* Trying to load gantt-chart
|
||||
* *(gantt)* Add task collection to useGanttFilter
|
||||
* *(gantt)* Use time constants
|
||||
* *(gantt)* Reset gantt filter
|
||||
* *(gantt)* Disable useDayjsLanguageSync
|
||||
* *(link shares)* Hide the logo if a query parameter was passed
|
||||
* *(link shares)* Allows switching the initial view by passing a query parameter
|
||||
* *(link shares)* Cleanup link share table
|
||||
* *(link shares)* Allows switching the initial view by passing a query parameter (#2335)
|
||||
* *(list)* Add info dialoge to show list description (#2368)
|
||||
* *(openid)* Show error message from query after being redirected from third party
|
||||
* *(task)* Cover image for tasks (#2460)
|
||||
* *(tests)* Add tests for task attachments* Settings background script setup (#2104) ([ff65580](ff655808b3cb562bd1c843ff70bf3641718ae61d))
|
||||
* List settings edit script setup (#1988) ([f6437c8](f6437c81da73b7e3406c28b9bd7b201e376f15c3))
|
||||
* Convert abstractService to ts ([74ad6e6](74ad6e65e88d6aa5702686dd0b6f55e2dc6b7b77))
|
||||
* Add properties to models ([797de0c](797de0c5432face3887f4d77bcb7dd7ee2e7e0c1))
|
||||
* Constants ([8fb0065](8fb00653e47c6f41a0e461c944b401d58b4a2351))
|
||||
* Function attribute typing ([332acf0](332acf012c423d3201ec1811093226447cd065e8))
|
||||
* Improve types ([c9e85cb](c9e85cb52b562cf9dcfac3ed54d8289e2b499992))
|
||||
* Improve store and model typing ([3766b5e](3766b5e51ba9c40a6affa91ce5cc11519e2da5c3))
|
||||
* Use lib ESNext setting for typescript ([79e7e4a](79e7e4a8aefe9f4d00bcbad76c4206c409384b61))
|
||||
* Extend mode interface from class instead from interface ([a6b96f8](a6b96f857d949874ba75f657b887a7c997aa7c57))
|
||||
* Improve store typing ([2444784](244478400ad8b8243ae2b29d741c03fa2b83601b))
|
||||
* Add modelTypes ([7d4ba62](7d4ba6249e300b6711369476f5d6a84728668b0f))
|
||||
* Convert services and models to ts (#1798) ([dbea1f7](dbea1f7a51f3cf5173b5f381944c4ef19ef97ec8))
|
||||
* Add sponsor logo to readme (relm) ([e959043](e95904351fbd30776306225f3be55978d70ae42e))
|
||||
* Show user display name when searching for assignees on a list ([65fd2f1](65fd2f14a067ea9d79b352af00f3c316be883fdf))
|
||||
* Add keyboard shortcut to toggle task description edit (#2332) ([7f6f896](7f6f8963e7db236f3beb9e6a36fab4ba479b969b))
|
||||
* Programmatically generate list of available views ([26d02d5](26d02d5593283c3ad2fb961348ba2f412cc9eaa8))
|
||||
* Add fallback for useCopyToClipboard (#2343) ([7b398f7](7b398f73f604d6564a41c3ce5031883c677f02c7))
|
||||
* Improve models ([1a11b43](1a11b43ca8d51bf998019fbc741e845b07d70157))
|
||||
* Use v-model more consequent (#2356) ([db8b881](db8b8812af731fb6acbdd1aec173e37b84066eea))
|
||||
* Make share link name italic ([224cea3](224cea33ced403f45c7d833ab576be44c89d199a))
|
||||
* Move the url link to the bottom of the items ([6576b61](6576b6148ce1b02dbe6a335778592c4b72e275de))
|
||||
* Color the task color button when the task has a color set ([51c806c](51c806c12b90aa124384497856590f5010b9ff49))
|
||||
* Color the color button icon instead of the button itself ([bdf992c](bdf992c9bfe9de176a22f7b5a6fdae1bc5e5010f))
|
||||
* Move the update available dialoge always to the bottom ([a18c6ab](a18c6ab8d860a496905f58278315222992bacd07))
|
||||
* Show the task color bubble everywhere ([2683fec](2683fec0a67f6afd16579bb44a6ceadc0edd565f))
|
||||
* Color the task color button when the task has a color set (#2331) ([f70b1d2](f70b1d2902f91a88eaf33f1a9799489c20a6a143))
|
||||
* Namespace settings archive script setup ([ad6b335](ad6b335d41e07e8ce2e74e4282d572ba4c04ea30))
|
||||
* ListNamespaces script setup (#2389) ([ff5d1fc](ff5d1fc8c1961134ef3baec09be52b02c0b6898e))
|
||||
* NewTeam script setup (#2388) ([e91b5fd](e91b5fde0216e15f739da22efbcaae3829e31ba1))
|
||||
* Port label store to pinia | pinia 1/9 (#2391) ([d67e5e3](d67e5e386d7d1901694fe0004f580807754bcae1))
|
||||
* Use pnpm ([d76b526](d76b526916d4aca279670d2690f7bb8e63e432a7))
|
||||
* Move list store to pina (#2392) ([a38075f](a38075f376aa5cc2d8a06943cf8932366a0d4011))
|
||||
* Task relatedTasks script setup ([943d5f7](943d5f79757b73f447c51641812e7766edeffe9e))
|
||||
* Allow marking a related task done directly from the list ([ce0f58c](ce0f58c7833bbb37974709112cdedad88ae07cc8))
|
||||
* DeleteNamespace script setup (#2387) ([0814890](0814890cac92b813b5b93bb42c7a40e2dc13cb94))
|
||||
* Task relatedTasks script setup (#1939) ([d57e27b](d57e27b4a62aaa0f0a739f030515fff72a56f7fc))
|
||||
* Use pnpm (#1789) ([f7ca064](f7ca064127863de4a4c1e3ae29d84d6bd5311cb9))
|
||||
* Add hot reloading support ([1c58fcc](1c58fccd926586b2303ce41939a535b2044a78a9))
|
||||
* Move namespaces store to stores ([9474240](9474240cb9159a0e1b42f82cb492cc267782ce4f))
|
||||
* Port namespace store to pinia ([093ab76](093ab766d45247b3b1d12740dc6b24c6b48f21c4))
|
||||
* Feat-attachments-script-setup (#2358) ([4dfcd8e](4dfcd8e70f54d2ed977d4b8de5fb8bf9469819aa))
|
||||
* Convert namespaces store to pina (#2393) ([937fd36](937fd36f724f2b383fe51ae25a55ba90f58c8975))
|
||||
* Move attachments store to stores ([c2ba1b2](c2ba1b2828439d3bd1e846a4bb9a4c456562c460))
|
||||
* Port attachments store to pinia ([20e9420](20e94206388ab694248942996fdb67b7be87e76f))
|
||||
* Move config to stores ([9e8c429](9e8c429864923215be5b110fdcb7c4a586c60f3d))
|
||||
* Port config store to pinia ([a737fc5](a737fc5bc2affc87b209746ecf04c66e1f6077db))
|
||||
* Filter-popup script setup (#2418) ([ba2605a](ba2605af1bb6f9ba7d3bd1b99ed862d510c6bb31))
|
||||
* ListLabels script setup (#2416) ([89e428b](89e428b4d285f3465a40773fbda564c432fb371e))
|
||||
* Possible fix for pnpm ci errors ([e8f0b56](e8f0b5665161e77bcc961ec0dc57c5b127b93a1f))
|
||||
* NewLabel script setup (#2414) ([7f581cb](7f581cbe2780633fdfa03609824182fe93fe77e3))
|
||||
* Possible fix for pnpm ci errors (#2413) ([bc83309](bc833091f2b919177ce75815b562818c93ea2884))
|
||||
* Feat NewNamespace script setup (#2415) ([63f2e6b](63f2e6ba6f22502becf61aa89c729fa9d01cdc7b))
|
||||
* ListList script setup (#2441) ([bbf4ef4](bbf4ef4697fc6338ad603e2491fe4aed61057cd8))
|
||||
* Move auth to stores ([f30c964](f30c964c06987f87b615c3eec25197241175db96))
|
||||
* Port auth store to pinia ([7b53e68](7b53e684aa405a7874f189dcb404c031dfed1388))
|
||||
* Auth store type improvements ([176ad56](176ad565cc64e2212eedb1601c844e458d7e4bb6))
|
||||
* Improve api-config (#2444) ([8f25f5d](8f25f5d353064f383e97bbc524ce6e00ba559d0f))
|
||||
* Convert model methods to named functions ([8e3f54a](8e3f54ae42c21fdae62225892ad340877651df27))
|
||||
* Migrate auth store to pina (#2398) ([9856fab](9856fab38f62f82a42d5cb3b69b232eb319b8050))
|
||||
* Move tasks to stores ([1fdda07](1fdda07f650702b7e3943e0afc7532367ee20100))
|
||||
* Port tasks store to pinia ([34ffd1d](34ffd1d5729341bdede217387a4a4c490d7d60d8))
|
||||
* Move kanban to stores ([9f26ae1](9f26ae1ee6241b2ef529f01d3511380c9d7a4576))
|
||||
* Port kanban store to pinia ([c35810f](c35810f28fc5aacefabad7526b0ac4e982d53cc7))
|
||||
* Port tasks store to pina (#2409) ([8c394d8](8c394d8024a825b961e825543453d188c28fa370))
|
||||
* Automatically create subtask relations based on indention ([cc378b8](cc378b83fee2b326610cdda1997cc5236f947fbf))
|
||||
* Automatically create subtask relations based on indention (#2443) ([ec227a6](ec227a6872ababb612cb0b7e68ca0c20676117c1))
|
||||
* Migrate kanban store to pina (#2411) ([d1d7cd5](d1d7cd535ed992fc0a8be8afaf13250ac9b61132))
|
||||
* Move base store to stores ([df74f9d](df74f9d80cdd44315a29189ecb2f236482cb70f5))
|
||||
* Port base store to pinia ([7f281fc](7f281fc5e98c5eb83f926100c7f79ee374c5a784))
|
||||
* Rework loading state of stores ([1d7f857](1d7f857070651f676bbb5bd7e6d79c7fed56be5f))
|
||||
* TaskDetail as script setup (#1792) ([2dc36c0](2dc36c032bad93654fbd64a68682685870972feb))
|
||||
* Add github issue template ([9400637](940063784b3ec129e99fe18c4eb2b205ffb15163))
|
||||
* Login script setup (#2417) ([63fb8a1](63fb8a1962f9ecd8c9a079e2770b4658c5559d84))
|
||||
* Datepicker script setup (#2456) ([ff1968a](ff1968aa36254d788d0d80ba2d156ce66f4a9df8))
|
||||
* Multiselect script setup (#2458) ([0620b8f](0620b8f0b308e358526bed0d82322ffb9c0627cf))
|
||||
* ColorPicker script setup (#2457) ([b08dd58](b08dd58552edb763f007f355f5c0d36d6dccbd05))
|
||||
* Migrate kanban card to script setup ([a5925ba](a5925baff03ac2809b7c601b45b93363b6188083))
|
||||
* Migrate kanban card to script setup (#2459) ([3e21a8e](3e21a8ed6ee74d85628feedd8855c817af8de538))
|
||||
* Add nix flake for dev shell ([12215c0](12215c043d45d2f2294e65671587a923997e6f6f))
|
||||
* Fancycheckbox script setup (#2462) ([06c1a54](06c1a548867e37a74a8493bd44fef728e10c658b))
|
||||
* Editor script setup ([db627ed](db627ed28af8432e6971ad08864d11e56d3512c6))
|
||||
* Use floating-ui (#2482) ([f360ebf](f360ebfe9854aeae9cb426c67b1bb48aa74a9c08))
|
||||
* Update eslint config ([4655e1c](4655e1ce34223337c953ebbe52f94ef811034e6b))
|
||||
* Feature/update-eslint-config (#2484) ([6f2dedc](6f2dedcb488ec6a38182e85e702ec880263ecbd3))
|
||||
* Move composables in separate files (#2485) ([c206fc6](c206fc6f3462be2e0ebc0bd16d96b3c0099fdda1))
|
||||
* Add display of kanban card attachment image ([3d88fda](3d88fdaaddca15b98efa938f0b2813420d56ad84))
|
||||
* Promote an attachment to task cover image ([877e425](877e4250554b31db2d57f44a7443c5d04c783e59))
|
||||
* Add indicator if an attachment is task cover ([f01107f](f01107fd737e2205bf60498b3d2954a251c3d9d4))
|
||||
* Show done tasks as strikethrough when searching for new tasks to relate ([74a9b9a](74a9b9ab1b31740fe84a7dddd91a04995c1eb58d))
|
||||
* Allow users to leave a team they're in ([feeaca2](feeaca2c02fb233c35a81f786acd5cbdf5c5d21d))
|
||||
* Add TickTick migrator support ([1af4f78](1af4f7811a63826c4aa4740a55f606757e22c7ae))
|
||||
* Make salutation i18n static ([c20de51](c20de51a3c98792580c0a2f2751648582ac5ac0c))
|
||||
* Get username from store getter ([c4d7f6f](c4d7f6fdfa18c221597b28198d5fa432b1e934dc))
|
||||
* Use getter and helper in other components as well ([9de20b4](9de20b4c54d192a20f9135388de9fa13121ed322))
|
||||
* Make salutation i18n static (#2546) ([29f6874](29f68747bbd7da50d37ae3238b6b19782ec8022b))
|
||||
* Refactor password reset to use a single password field ([4ed665f](4ed665fbd9dc4db1ecb6afc1a75d1818c3518186))
|
||||
* Rename useTaskList ([7ce8802](7ce880239ec3ce16313d93bfefa657c499bbfb29))
|
||||
* Add basic implementation of ganttastic ([2b0df8c](2b0df8c2375ec5f9afe43207807e999bcc693d21))
|
||||
* Allow passing props down to the gantt component ([49a2497](49a24977f96cff1e90e706321505ae43bf7efadf))
|
||||
* Only load tasks which start in the currently selected range ([ed241d2](ed241d21bea91795a10cdc1af92561d435c9eedc))
|
||||
* Dynamically set default date ([736e5a8](736e5a8bf55ccf7cbed23fd3af48122c459bcdc6))
|
||||
* Dynamically set default date ([3b48ada](3b48adad675b0b20dc91a08f8ebbfe1dd1c3806b))
|
||||
* Create new tasks ([ef46893](ef4689335b3e738b7e1338657e9dcd69c82fbcb9))
|
||||
* Add open task detail when double clicking ([d2c4092](d2c40926ded479db92d0f3b77d2ece5842bcacbb))
|
||||
* Scroll ([c8eac91](c8eac914d10a09453afb70d35c6d16faac9cd00c))
|
||||
* Styling ([80c151c](80c151ca6c4a76a5f912505672eee471f77a3bba))
|
||||
* Update task in gantt bar after dragging to make sure it changes its color ([ebd824b](ebd824bddf8d37a66d2dbf7f330b39c8849db9b2))
|
||||
* Show done tasks strikethrough ([3eacc07](3eacc0754ff50fed2d5a50198480c5c8d697f6ce))
|
||||
* Handle changing props ([29dcc02](29dcc02217dfe9d52b3cdd6166ca82cc8be1022e))
|
||||
* Loading animation ([8c62a9e](8c62a9e198fb5b8221a13747e9510f5036ed3095))
|
||||
* Create task when pressing the button ([0a9588e](0a9588e09730e83ddc61630012e62c0530a9997d))
|
||||
* Increase the default date range ([5f7159e](5f7159ebc49e73bc4757c7cefa9a10ed14d65b46))
|
||||
* Only use one watcher ([64fdae8](64fdae81ec8a1b807a1b1788a6954c8d7850dc36))
|
||||
* Review changes ([f21a4e1](f21a4e1e9f558e999e1f6638847aeab4d73b9636))
|
||||
* Update ganttastic version ([2f820e5](2f820e517f6dea384440a9574da4f82c02c86143))
|
||||
* Improve types ([3b244df](3b244dfdbecf2f1feaa766b5c9e52c7e66dfe52a))
|
||||
* Working route sync ([acdbf2f](acdbf2f8f5b8e28e923d7598696dadec373c7a67))
|
||||
* Working gantt-chart ([eaf7778](eaf777864ac857275bc657bf39f1886460d307d2))
|
||||
* Abstract to useGanttFilter / and useRouteFilter ([2c732eb](2c732eb0d55c9161b8d47cbc850421136994bff4))
|
||||
* Simplify ListGantt styles ([c7dd20e](c7dd20ef57f037db0ac8bbdc583463ae98ffe9ac))
|
||||
* Move useGanttTaskList in separate file ([7f4114b](7f4114b7032c24d9305c7c731ad1fef2f9390dcd))
|
||||
* Remove gantt-chart wrapper ([aefda38](aefda38bdd8fa5f5b4f4d2c7486566f669dd6929))
|
||||
* Use PascalCase for component name ([acb3ddc](acb3ddc73fd7a8240d42774c80c68b5a725c3734))
|
||||
* Use ref for filters ([51dc123](51dc123d893517a30c2dbb26a68e877b493ec95e))
|
||||
* Use plural for filters consequently ([6bf6357](6bf6357cbd281fa5b99b7aae9845fee90c758ae7))
|
||||
* Move config preparation in separate function ([e74e6fc](e74e6fcc996cced93f040782ac278db6baea975e))
|
||||
* Align with vue-flatpickr-component 10 ([874dc1e](874dc1e5fc9f76ad3d45f555b9d04585cd9a2704))
|
||||
* Replace our home-grown gantt implementation with ganttastic (#2180) ([fd3e7e6](fd3e7e655dbbd59f9a94db0f18a3ef4876cec059))
|
||||
* Improve useTaskList (#2582) ([d5258b7](d5258b73153a477a82c750482a6fd504c5823b7a))
|
||||
* Unify savedFilter logic in service (#2491) ([9807858](9807858436e4b7d6de8dcb71b2a03a55ed8a7d52))
|
||||
* Quick-actions script setup (#2478) ([386fd79](386fd79b4983b9d472d46219fc60c1a1a2cc1012))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(ci)* Sign drone config
|
||||
* *(ci)* Sign drone config
|
||||
* *(gantt)* Wip daterange
|
||||
* *(gantt)* Upgrade packages
|
||||
* *(gantt)* Upgrade packages
|
||||
* *(gantt)* Pnpm install after merge
|
||||
* *(i18n)* Use global scope
|
||||
* *(task)* Move cover image setter to store* Improve type imports ([af630d3](af630d3b8c1536c1a9a320172aaf19e000bb2517))
|
||||
* Remove date mixins ([b0ee316](b0ee316a262ca71b9cfecbaaeccab7f9465ec09d))
|
||||
* Remove global mixing ([4a247b2](4a247b2a7d6741bfec9fbdb387c9313d7b6381d1))
|
||||
* Remove unnecessary defineComponent ([6f93d63](6f93d6343c1c518fec3591b83b999efcbccf9607))
|
||||
* Better variable typing ([42e72d1](42e72d14a4a804aa38908cc2a9d6b4cb120c988a))
|
||||
* Align docker cypress image version with drone ([2445f0e](2445f0eec8b130d8d71e5fc399a399c0d1cf6836))
|
||||
* Minor fixes ([49f3b92](49f3b928cbc16031cf65fa3ed1cc908968e1083b))
|
||||
* Automerge renovate dev dependency updates ([d822709](d822709991ee4dc52ee8aa56a03248c6e4a3a709))
|
||||
* Rearrange non-dev dependencies ([b8d77a6](b8d77a617b0b205fbf8553d3ce060547c96f0f22))
|
||||
* Remove ([d91d1fe](d91d1fecf1b34734ef8af21c3c34bdaaa6d53e09))
|
||||
* Remove unused id ([5f678e2](5f678e2449529758cd6ade233c52a0c091889fd9))
|
||||
* Set more expressive variable names for available views dropdowns ([7e7fa80](7e7fa807fd1c6a34c5236cca4fb20141ca9d0454))
|
||||
* Improve types ([6d9c4a7](6d9c4a7aa083425e252b96729b57c16ab13fd295))
|
||||
* Don't cache node_modules ([b542221](b542221dac6a14cd84aab446ceab0888bc98bb38))
|
||||
* Don't use node alpine image ([6624db1](6624db1d49545524083d124698fa5b6e02bbfb0c))
|
||||
* Use node alpine image ([dfb3561](dfb3561310bec49043a630136a2d51cc80184cc1))
|
||||
* Optimise loading order (#2435) ([ca899d3](ca899d3b5172be6f39a60bdaffab58330225ecd9))
|
||||
* Make const out of export download file name (#2436) ([878c6ea](878c6ea9e17527b3f199f4acf10588e910b5727c))
|
||||
* Spread title ([3970d0f](3970d0fd315488427df0c4a37447eb52dca322b4))
|
||||
* Use better variable names ([8ce242b](8ce242bb6595ef12442a6ba0fb37eb66c65dd71b))
|
||||
* Break earlier if index === 0 ([d58f8b4](d58f8b4ba1d873abb0fc8dc4c2cec64a33b55ab8))
|
||||
* Use jsDoc to explain param ([5bd7c77](5bd7c77b68f08ab4771f3d80d5191def9d634204))
|
||||
* Small review adjustments ([af7f840](af7f8400e901c2f4d9c5c4cca7614af62892a75e))
|
||||
* Remove unneeded this from PasswordReset.vue (#2473) ([c232170](c2321703a767395b77523d4551ea508396b7cae8))
|
||||
* Remove IE edge fallback (#2477) ([3248dcd](3248dcd6636627548f2df869900a7943c0dde0ba))
|
||||
* Add line-wrap ([eb80bfa](eb80bfa00de891ee12643d664e8610d1f3bc851f))
|
||||
* Better wording for cover set button ([a773137](a7731370a0bcdd8a393036a617dd1953cd39f5df))
|
||||
* Update happy-dom less frequently ([458df80](458df8044306642e5da813ff8341bed07f67f26a))
|
||||
* Move helper function outside of composable ([aa2278a](aa2278a56411dc8045fa468b090755cf5d899d09))
|
||||
* Use flatpickr range instead of two datepickers ([c289a6a](c289a6ae18fd5936b789270cc72408374a790edc))
|
||||
* Use width property ([7a7a1c9](7a7a1c985e0feb8de62ddbdd54f36f2a09a9d765))
|
||||
* Remove old component and dependencies ([6cb331e](6cb331ee0f26dffcbf700426da17acb6159aea3e))
|
||||
* Use Loading component ([766b4c6](766b4c669ff52f6d6c888727e62142eaa90de54d))
|
||||
* Use @/models ([d3925b8](d3925b8d80e16e25e9b82d057fb47ed9f41f61a0))
|
||||
* Uppercase const ([98d0398](98d0398ca840d8d8077f850c8ca4e65784373b61))
|
||||
* Don't set required if there's a default value ([ed5d3be](ed5d3be7cba7992eb18a3ed1844c085cf88b3bdd))
|
||||
* Define types ([56a2573](56a25734d7557663e2ba43ba41f4922f0b10ed8b))
|
||||
* Don't use for..in ([6975a2b](6975a2b286628294b8909bce3d43334cc383d987))
|
||||
* Add types for template ref ([4be0977](4be097701449b74bbeb7218b539db65961539591))
|
||||
* Don't use ref when not nessecary ([fd9d0ad](fd9d0ad1553756414696315508bc2d8928f63d9d))
|
||||
* Update lockfile ([957d8f0](957d8f05a5e9548138f8dce192513928deb02669))
|
||||
* Better naming for input ([df02dd5](df02dd529181e9701ce586dba9025c83eeaf48d8))
|
||||
* Clean up ([2acb70c](2acb70c56257202fe7d136b36ceaaa2fe122491e))
|
||||
* Pnpm install after merge ([26e522c](26e522cf8c302f5d63b26134e5fa37bed5c808ef))
|
||||
* Use vue-ganttastic release ([6c61907](6c619072b4863328c24588bb08a9543806942be1))
|
||||
* Don't pass other params to ListGantt than route ([cf0eaf9](cf0eaf9ba1816b610ba1cbc9b4a6c661f00f61a5))
|
||||
* Refactor parseTimeLabel to own function ([443e1a0](443e1a063dfff3cbb82a9f625e05bf7e2b606cbe))
|
||||
* Add git-cliff to flake ([b817720](b817720907b0c4bb848e9624e3fdf71437ba0bde))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* *(other)* [skip ci] Updated translations via Crowdin
|
||||
|
||||
|
||||
## [0.19.1] - 2022-08-17
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||
[![Download](https://img.shields.io/badge/download-v0.19.1-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.20.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||
|
||||
This is the web frontend for Vikunja, written in Vue.js.
|
||||
|
|
|
@ -11,7 +11,7 @@ describe('List View Gantt', () => {
|
|||
const tasks = TaskFactory.create(1)
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart .tasks')
|
||||
cy.get('.g-gantt-rows-container')
|
||||
.should('not.contain', tasks[0].title)
|
||||
})
|
||||
|
||||
|
@ -25,7 +25,7 @@ describe('List View Gantt', () => {
|
|||
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart .months')
|
||||
cy.get('.g-timeunits-container')
|
||||
.should('contain', format(now, 'MMMM'))
|
||||
.should('contain', format(nextMonth, 'MMMM'))
|
||||
})
|
||||
|
@ -38,14 +38,13 @@ describe('List View Gantt', () => {
|
|||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart .tasks')
|
||||
cy.get('.g-gantt-rows-container')
|
||||
.should('not.be.empty')
|
||||
cy.get('.gantt-chart .tasks')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Shows tasks with no dates after enabling them', () => {
|
||||
TaskFactory.create(1, {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
})
|
||||
|
@ -55,13 +54,15 @@ describe('List View Gantt', () => {
|
|||
.contains('Show tasks which don\'t have dates set')
|
||||
.click()
|
||||
|
||||
cy.get('.gantt-chart .tasks')
|
||||
cy.get('.g-gantt-rows-container')
|
||||
.should('not.be.empty')
|
||||
cy.get('.gantt-chart .tasks .task.nodate')
|
||||
.should('exist')
|
||||
.should('contain', tasks[0].title)
|
||||
})
|
||||
|
||||
it('Drags a task around', () => {
|
||||
cy.intercept('**/api/v1/tasks/*')
|
||||
.as('taskUpdate')
|
||||
|
||||
const now = new Date()
|
||||
TaskFactory.create(1, {
|
||||
start_date: formatISO(now),
|
||||
|
@ -69,10 +70,11 @@ describe('List View Gantt', () => {
|
|||
})
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart .tasks .task')
|
||||
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
|
||||
.first()
|
||||
.trigger('mousedown', {which: 1})
|
||||
.trigger('mousemove', {clientX: 500, clientY: 0})
|
||||
.trigger('mouseup', {force: true})
|
||||
cy.wait('@taskUpdate')
|
||||
})
|
||||
})
|
|
@ -5,6 +5,6 @@
|
|||
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
in {
|
||||
defaultPackage.x86_64-linux =
|
||||
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress ]; };
|
||||
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress pkgs.git-cliff ]; };
|
||||
};
|
||||
}
|
||||
|
|
68
package.json
68
package.json
|
@ -23,22 +23,25 @@
|
|||
"@fortawesome/free-solid-svg-icons": "6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@kyvg/vue3-notification": "2.4.1",
|
||||
"@sentry/tracing": "7.16.0",
|
||||
"@sentry/vue": "7.16.0",
|
||||
"@infectoone/vue-ganttastic": "2.1.2",
|
||||
"@kyvg/vue3-notification": "2.6.1",
|
||||
"@sentry/tracing": "7.17.4",
|
||||
"@sentry/vue": "7.17.4",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/sortablejs": "1.15.0",
|
||||
"@vueuse/core": "9.3.1",
|
||||
"@vueuse/router": "9.3.1",
|
||||
"@vueuse/core": "9.4.0",
|
||||
"@vueuse/router": "9.4.0",
|
||||
"axios": "0.27.2",
|
||||
"blurhash": "2.0.3",
|
||||
"blurhash": "2.0.4",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.65.9",
|
||||
"date-fns": "2.29.3",
|
||||
"dayjs": "1.11.6",
|
||||
"dompurify": "2.4.0",
|
||||
"easymde": "2.18.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.21",
|
||||
"floating-vue": "2.0.0-beta.20",
|
||||
|
@ -46,7 +49,7 @@
|
|||
"is-touch-device": "1.0.1",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"marked": "4.1.1",
|
||||
"marked": "4.2.2",
|
||||
"minimist": "1.2.7",
|
||||
"pinia": "2.0.23",
|
||||
"register-service-worker": "1.7.2",
|
||||
|
@ -55,56 +58,57 @@
|
|||
"ufo": "0.8.6",
|
||||
"vue": "3.2.41",
|
||||
"vue-advanced-cropper": "2.8.6",
|
||||
"vue-drag-resize": "2.0.3",
|
||||
"vue-flatpickr-component": "10.0.0",
|
||||
"vue-flatpickr-component": "11.0.1",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.5",
|
||||
"vue-router": "4.1.6",
|
||||
"workbox-precaching": "6.5.4",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.1",
|
||||
"@cypress/vite-dev-server": "3.3.1",
|
||||
"@cypress/vue": "4.2.1",
|
||||
"@cypress/vite-dev-server": "3.4.0",
|
||||
"@cypress/vue": "4.2.2",
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@rushstack/eslint-patch": "1.2.0",
|
||||
"@types/codemirror": "5.60.5",
|
||||
"@types/dompurify": "2.3.4",
|
||||
"@types/flexsearch": "0.7.3",
|
||||
"@types/lodash.debounce": "4.0.7",
|
||||
"@types/marked": "4.0.7",
|
||||
"@types/node": "16.11.68",
|
||||
"@types/node": "18.11.9",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.40.1",
|
||||
"@typescript-eslint/parser": "5.40.1",
|
||||
"@vitejs/plugin-legacy": "2.2.0",
|
||||
"@vitejs/plugin-vue": "3.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.42.0",
|
||||
"@typescript-eslint/parser": "5.42.0",
|
||||
"@vitejs/plugin-legacy": "2.3.1",
|
||||
"@vitejs/plugin-vue": "3.2.0",
|
||||
"@vue/eslint-config-typescript": "11.0.2",
|
||||
"@vue/test-utils": "2.1.0",
|
||||
"@vue/test-utils": "2.2.1",
|
||||
"@vue/tsconfig": "0.1.3",
|
||||
"autoprefixer": "10.4.12",
|
||||
"autoprefixer": "10.4.13",
|
||||
"browserslist": "4.21.4",
|
||||
"caniuse-lite": "1.0.30001420",
|
||||
"cypress": "10.10.0",
|
||||
"esbuild": "0.15.12",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-plugin-vue": "9.6.0",
|
||||
"caniuse-lite": "1.0.30001430",
|
||||
"csstype": "3.1.1",
|
||||
"cypress": "10.11.0",
|
||||
"esbuild": "0.15.13",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-plugin-vue": "9.7.0",
|
||||
"express": "4.18.2",
|
||||
"happy-dom": "7.5.12",
|
||||
"netlify-cli": "12.0.9",
|
||||
"happy-dom": "7.6.6",
|
||||
"netlify-cli": "12.1.0",
|
||||
"postcss": "8.4.18",
|
||||
"postcss-preset-env": "7.8.2",
|
||||
"rollup": "3.2.3",
|
||||
"rollup": "3.2.5",
|
||||
"rollup-plugin-visualizer": "5.8.3",
|
||||
"sass": "1.55.0",
|
||||
"sass": "1.56.0",
|
||||
"typescript": "4.8.4",
|
||||
"vite": "3.1.8",
|
||||
"vite-plugin-pwa": "0.13.1",
|
||||
"vite": "3.2.3",
|
||||
"vite-plugin-pwa": "0.13.2",
|
||||
"vite-svg-loader": "3.6.0",
|
||||
"vitest": "0.24.3",
|
||||
"vitest": "0.24.5",
|
||||
"vue-tsc": "1.0.9",
|
||||
"wait-on": "6.0.1",
|
||||
"workbox-cli": "6.5.4"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "pnpm@7.14.0"
|
||||
"packageManager": "pnpm@7.14.2"
|
||||
}
|
||||
|
|
1318
pnpm-lock.yaml
1318
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,18 +1,53 @@
|
|||
<!-- a disabled link of any kind is not a link -->
|
||||
<!-- we have a router link -->
|
||||
<!-- just a normal link -->
|
||||
<!-- a button it shall be -->
|
||||
<!-- note that we only pass the click listener here -->
|
||||
<template>
|
||||
<component
|
||||
:is="componentNodeName"
|
||||
<div
|
||||
v-if="disabled === true && (to !== undefined || href !== undefined)"
|
||||
class="base-button"
|
||||
:class="{ 'base-button--type-button': isButton }"
|
||||
v-bind="elementBindings"
|
||||
:disabled="disabled || undefined"
|
||||
:aria-disabled="disabled || undefined"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
</component>
|
||||
</div>
|
||||
<router-link
|
||||
v-else-if="to !== undefined"
|
||||
:to="to"
|
||||
class="base-button"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
</router-link>
|
||||
<a v-else-if="href !== undefined"
|
||||
class="base-button"
|
||||
:href="href"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
</a>
|
||||
<button
|
||||
v-else
|
||||
:type="type"
|
||||
class="base-button base-button--type-button"
|
||||
:disabled="disabled || undefined"
|
||||
ref="button"
|
||||
@click="(event: MouseEvent) => emit('click', event)"
|
||||
>
|
||||
<slot/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default { inheritAttrs: false }
|
||||
const BASE_BUTTON_TYPES_MAP = {
|
||||
BUTTON: 'button',
|
||||
SUBMIT: 'submit',
|
||||
} as const
|
||||
|
||||
export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUTTON_TYPES_MAP] | undefined
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -20,77 +55,36 @@ export default { inheritAttrs: false }
|
|||
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
|
||||
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
|
||||
|
||||
// the component tries to heuristically determine what it should be checking the props (see the
|
||||
// componentNodeName and elementBindings ref for this).
|
||||
// the component tries to heuristically determine what it should be checking the props
|
||||
|
||||
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
|
||||
|
||||
import { ref, watchEffect, computed, useAttrs, type PropType } from 'vue'
|
||||
import {unrefElement} from '@vueuse/core'
|
||||
import {ref, type HTMLAttributes} from 'vue'
|
||||
import type {RouteLocationNamedRaw} from 'vue-router'
|
||||
|
||||
const BASE_BUTTON_TYPES_MAP = Object.freeze({
|
||||
button: 'button',
|
||||
submit: 'submit',
|
||||
})
|
||||
|
||||
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<BaseButtonTypes>,
|
||||
default: 'button',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const componentNodeName = ref<Node['nodeName']>('button')
|
||||
|
||||
interface ElementBindings {
|
||||
type?: string;
|
||||
rel?: string;
|
||||
target?: string;
|
||||
export interface BaseButtonProps extends HTMLAttributes {
|
||||
type?: BaseButtonTypes
|
||||
disabled?: boolean
|
||||
to?: RouteLocationNamedRaw
|
||||
href?: string
|
||||
}
|
||||
|
||||
const elementBindings = ref({})
|
||||
export interface BaseButtonEmits {
|
||||
(e: 'click', payload: MouseEvent): void
|
||||
}
|
||||
|
||||
const attrs = useAttrs()
|
||||
watchEffect(() => {
|
||||
// by default this component is a button element with the attribute of the type "button" (default prop value)
|
||||
let nodeName = 'button'
|
||||
let bindings: ElementBindings = {type: props.type}
|
||||
const {
|
||||
type = BASE_BUTTON_TYPES_MAP.BUTTON,
|
||||
disabled = false,
|
||||
} = defineProps<BaseButtonProps>()
|
||||
|
||||
// if we find a "to" prop we set it as router-link
|
||||
if ('to' in attrs) {
|
||||
nodeName = 'router-link'
|
||||
bindings = {}
|
||||
}
|
||||
const emit = defineEmits<BaseButtonEmits>()
|
||||
|
||||
// if there is a href we assume the user wants an external link via a link element
|
||||
// we also set a predefined value for the attribute rel, but make it possible to overwrite this by the user.
|
||||
if ('href' in attrs) {
|
||||
nodeName = 'a'
|
||||
bindings = {
|
||||
rel: 'noreferrer noopener nofollow',
|
||||
target: '_blank',
|
||||
}
|
||||
}
|
||||
|
||||
componentNodeName.value = nodeName
|
||||
elementBindings.value = {
|
||||
...bindings,
|
||||
...attrs,
|
||||
}
|
||||
})
|
||||
|
||||
const isButton = computed(() => componentNodeName.value === 'button')
|
||||
|
||||
const button = ref()
|
||||
const button = ref<HTMLElement | null>(null)
|
||||
|
||||
function focus() {
|
||||
button.value.focus()
|
||||
unrefElement(button)?.focus()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -26,7 +26,7 @@ if (navigator && navigator.serviceWorker) {
|
|||
)
|
||||
}
|
||||
|
||||
function showRefreshUI(e) {
|
||||
function showRefreshUI(e: Event) {
|
||||
console.log('recieved refresh event', e)
|
||||
registration.value = e.detail
|
||||
updateAvailable.value = true
|
||||
|
|
|
@ -1,12 +1,3 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
import ErrorComponent from '@/components/misc/error.vue'
|
||||
import LoadingComponent from '@/components/misc/loading.vue'
|
||||
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
|
||||
|
||||
const Editor = () => import('@/components/input/editor.vue')
|
||||
|
||||
export default defineAsyncComponent({
|
||||
loader: Editor,
|
||||
loadingComponent: LoadingComponent,
|
||||
errorComponent: ErrorComponent,
|
||||
timeout: 60000,
|
||||
})
|
||||
export default createAsyncComponent(() => import('@/components/input/editor.vue'))
|
|
@ -9,64 +9,61 @@
|
|||
}
|
||||
]"
|
||||
>
|
||||
<icon
|
||||
v-if="showIconOnly"
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||
/>
|
||||
<span class="icon is-small" v-else-if="icon !== ''">
|
||||
<template v-if="icon">
|
||||
<icon
|
||||
v-if="showIconOnly"
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||
/>
|
||||
</span>
|
||||
<span class="icon is-small" v-else>
|
||||
<icon
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<slot />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
const BUTTON_TYPES_MAP = {
|
||||
primary: 'is-primary',
|
||||
secondary: 'is-outlined',
|
||||
tertiary: 'is-text is-inverted underline-none',
|
||||
} as const
|
||||
|
||||
export type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
|
||||
|
||||
export default { name: 'x-button' }
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, useSlots, type PropType} from 'vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import {computed, useSlots} from 'vue'
|
||||
import BaseButton, {type BaseButtonProps} from '@/components/base/BaseButton.vue'
|
||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
const BUTTON_TYPES_MAP = Object.freeze({
|
||||
primary: 'is-primary',
|
||||
secondary: 'is-outlined',
|
||||
tertiary: 'is-text is-inverted underline-none',
|
||||
})
|
||||
// extending the props of the BaseButton
|
||||
export interface ButtonProps extends BaseButtonProps {
|
||||
variant?: ButtonTypes
|
||||
icon?: IconProp
|
||||
iconColor?: string
|
||||
loading?: boolean
|
||||
shadow?: boolean
|
||||
}
|
||||
|
||||
type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
|
||||
const {
|
||||
variant = 'primary',
|
||||
icon = '',
|
||||
iconColor = '',
|
||||
loading = false,
|
||||
shadow = true,
|
||||
} = defineProps<ButtonProps>()
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String as PropType<ButtonTypes>,
|
||||
default: 'primary',
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array],
|
||||
default: '',
|
||||
},
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shadow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const variantClass = computed(() => BUTTON_TYPES_MAP[props.variant])
|
||||
const variantClass = computed(() => BUTTON_TYPES_MAP[variant])
|
||||
|
||||
const slots = useSlots()
|
||||
const showIconOnly = computed(() => props.icon !== '' && typeof slots.default === 'undefined')
|
||||
const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'undefined')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -193,7 +193,7 @@ function toggleDatePopup() {
|
|||
}
|
||||
|
||||
const datepickerPopup = ref<HTMLElement | null>(null)
|
||||
function hideDatePopup(e) {
|
||||
function hideDatePopup(e: MouseEvent) {
|
||||
if (show.value) {
|
||||
closeWhenClickedOutside(e, datepickerPopup.value, close)
|
||||
}
|
||||
|
|
|
@ -115,6 +115,7 @@ const props = defineProps({
|
|||
default: true,
|
||||
},
|
||||
bottomActions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
emptyText: {
|
||||
|
|
|
@ -100,37 +100,52 @@ function elementInResults(elem: string | any, label: string, query: string): boo
|
|||
}
|
||||
|
||||
const props = defineProps({
|
||||
// When true, shows a loading spinner
|
||||
/**
|
||||
* When true, shows a loading spinner
|
||||
*/
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// The placeholder of the search input
|
||||
/**
|
||||
* The placeholder of the search input
|
||||
*/
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// The search results where the @search listener needs to put the results into
|
||||
/**
|
||||
* The search results where the @search listener needs to put the results into
|
||||
*/
|
||||
searchResults: {
|
||||
type: Array as PropType<{[id: string]: any}>,
|
||||
default: () => [],
|
||||
},
|
||||
// The name of the property of the searched object to show the user.
|
||||
// If empty the component will show all raw data of an entry.
|
||||
/**
|
||||
* The name of the property of the searched object to show the user.
|
||||
* If empty the component will show all raw data of an entry.
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// The object with the value, updated every time an entry is selected.
|
||||
/**
|
||||
* The object with the value, updated every time an entry is selected.
|
||||
*/
|
||||
modelValue: {
|
||||
type: [Object] as PropType<{[key: string]: any}>,
|
||||
default: null,
|
||||
},
|
||||
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
|
||||
/**
|
||||
* If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
|
||||
*/
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// The text shown next to the new value option.
|
||||
/**
|
||||
* The text shown next to the new value option.
|
||||
*/
|
||||
createPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
|
@ -138,7 +153,9 @@ const props = defineProps({
|
|||
return t('input.multiselect.createPlaceholder')
|
||||
},
|
||||
},
|
||||
// The text shown next to an option.
|
||||
/**
|
||||
* The text shown next to an option.
|
||||
*/
|
||||
selectPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
|
@ -146,22 +163,30 @@ const props = defineProps({
|
|||
return t('input.multiselect.selectPlaceholder')
|
||||
},
|
||||
},
|
||||
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
|
||||
/**
|
||||
* If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
|
||||
*/
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// If true, displays the search results inline instead of using a dropdown.
|
||||
/**
|
||||
* If true, displays the search results inline instead of using a dropdown.
|
||||
*/
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// If true, shows search results when no query is specified.
|
||||
/**
|
||||
* If true, shows search results when no query is specified.
|
||||
*/
|
||||
showEmpty: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke.
|
||||
/**
|
||||
* The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke.
|
||||
*/
|
||||
searchDelay: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
|
@ -174,17 +199,25 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: null): void
|
||||
// @search: Triggered every time the search query input changes
|
||||
/**
|
||||
* Triggered every time the search query input changes
|
||||
*/
|
||||
(e: 'search', query: string): void
|
||||
// @select: Triggered every time an option from the search results is selected. Also triggers a change in v-model.
|
||||
(e: 'select', value: null): void
|
||||
// @create: If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
|
||||
/**
|
||||
* 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
|
||||
/**
|
||||
* 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
|
||||
// @remove: If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
|
||||
/**
|
||||
* 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
|
||||
}>()
|
||||
|
||||
const query = ref('')
|
||||
const query = ref<string | {[key: string]: any}>('')
|
||||
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
const localLoading = ref(false)
|
||||
const showSearchResults = ref(false)
|
||||
|
@ -414,8 +447,8 @@ function focus() {
|
|||
|
||||
.input-wrapper {
|
||||
padding: 0;
|
||||
background: var(--white) !important;
|
||||
border-color: var(--grey-200) !important;
|
||||
background: var(--white);
|
||||
border-color: var(--grey-200);
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
|
||||
|
|
|
@ -4,22 +4,28 @@
|
|||
class="vue-simplemde-textarea"
|
||||
:name="name"
|
||||
:value="modelValue"
|
||||
@input="handleInput($event.target.value)"
|
||||
@input="handleInput(($event.target as HTMLTextAreaElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch, onMounted, onDeactivated, onBeforeUnmount, nextTick, shallowReactive} from 'vue'
|
||||
import type { ShallowReactive } from 'vue'
|
||||
|
||||
import EasyMDE from 'easymde'
|
||||
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: String,
|
||||
name: String,
|
||||
previewClass: String,
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
previewClass: {
|
||||
type: String,
|
||||
},
|
||||
autoinit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
@ -37,7 +43,7 @@ const props = defineProps({
|
|||
default: () => ({}),
|
||||
},
|
||||
previewRender: {
|
||||
type: Function,
|
||||
type: Function as PropType<EasyMDE.Options['previewRender']>,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -51,9 +57,9 @@ onMounted(() => {
|
|||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
if (!easymde) return
|
||||
const isFullScreen = easymde.codemirror.getOption('fullScreen')
|
||||
if (isFullScreen) easymde.toggleFullScreen()
|
||||
if (easymde === undefined) return
|
||||
if (easymde.isFullscreenActive()) toggleFullScreen(easymde)
|
||||
easymde.toTextArea
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
@ -67,8 +73,8 @@ onBeforeUnmount(() => {
|
|||
const easymdeRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function initialize() {
|
||||
const configs = Object.assign({
|
||||
element: easymdeRef.value?.firstElementChild,
|
||||
const configs: EasyMDE.Options = Object.assign({
|
||||
element: easymdeRef.value?.firstElementChild as HTMLElement,
|
||||
initialValue: props.modelValue,
|
||||
previewRender: props.previewRender,
|
||||
renderingConfig: {},
|
||||
|
@ -81,7 +87,7 @@ function initialize() {
|
|||
|
||||
// Determine whether to enable code highlighting
|
||||
if (props.highlight) {
|
||||
configs.renderingConfig.codeSyntaxHighlighting = true
|
||||
configs.renderingConfig!.codeSyntaxHighlighting = true
|
||||
}
|
||||
|
||||
// Set whether to render the input html
|
||||
|
@ -92,15 +98,16 @@ function initialize() {
|
|||
|
||||
// Add a custom previewClass
|
||||
const className = props.previewClass || ''
|
||||
addPreviewClass(className)
|
||||
addPreviewClass(easymde, className)
|
||||
|
||||
// Binding event
|
||||
bindingEvents()
|
||||
easymde.codemirror.on('change', handleCodemirrorInput)
|
||||
easymde.codemirror.on('blur', handleCodemirrorBlur)
|
||||
|
||||
nextTick(() => emit('initialized', easymde))
|
||||
}
|
||||
|
||||
function addPreviewClass(className: string) {
|
||||
function addPreviewClass(easymde: EasyMDE, className: string) {
|
||||
const wrapper = easymde.codemirror.getWrapperElement()
|
||||
const preview = document.createElement('div')
|
||||
wrapper.nextSibling.className += ` ${className}`
|
||||
|
@ -108,28 +115,24 @@ function addPreviewClass(className: string) {
|
|||
wrapper.appendChild(preview)
|
||||
}
|
||||
|
||||
function bindingEvents() {
|
||||
easymde.codemirror.on('change', handleCodemirrorInput)
|
||||
easymde.codemirror.on('blur', handleCodemirrorBlur)
|
||||
function handleInput(val: string) {
|
||||
isValueUpdateFromInner.value = true
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
|
||||
function handleCodemirrorInput(instance, changeObj) {
|
||||
if (changeObj.origin === 'setValue') {
|
||||
function handleCodemirrorInput(instance: CodeMirror.Editor, changeObj: CodeMirror.EditorChange) {
|
||||
if (changeObj.origin === 'setValue' || easymde === undefined) {
|
||||
return
|
||||
}
|
||||
const val = easymde.value()
|
||||
handleInput(val)
|
||||
handleInput(easymde.value())
|
||||
}
|
||||
|
||||
function handleCodemirrorBlur() {
|
||||
const val = easymde.value()
|
||||
if (easymde === undefined) {
|
||||
return
|
||||
}
|
||||
isValueUpdateFromInner.value = true
|
||||
emit('blur', val)
|
||||
}
|
||||
|
||||
function handleInput(val) {
|
||||
isValueUpdateFromInner.value = true
|
||||
emit('update:modelValue', val)
|
||||
emit('blur', easymde.value())
|
||||
}
|
||||
|
||||
watch(
|
||||
|
@ -138,7 +141,7 @@ watch(
|
|||
if (isValueUpdateFromInner.value) {
|
||||
isValueUpdateFromInner.value = false
|
||||
} else {
|
||||
easymde.value(val)
|
||||
easymde?.value(val)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -78,12 +78,13 @@
|
|||
<script setup lang="ts">
|
||||
import {ref, computed, watchEffect, type PropType} from 'vue'
|
||||
|
||||
import {isSavedFilter} from '@/helpers/savedFilter'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import Subscription from '@/components/misc/subscription.vue'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
|
||||
import {isSavedFilter} from '@/services/savedFilter'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
|
|
@ -34,7 +34,7 @@ import {computed, ref, watch} from 'vue'
|
|||
|
||||
import Filters from '@/components/list/partials/filters.vue'
|
||||
|
||||
import {getDefaultParams} from '@/composables/taskList'
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
|
|
@ -212,7 +212,7 @@ import EditLabels from '@/components/tasks/partials/editLabels.vue'
|
|||
|
||||
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
import {getDefaultParams} from '@/composables/taskList'
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
import {camelCase} from 'camel-case'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskList.js
|
||||
|
|
|
@ -70,6 +70,8 @@ import {
|
|||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
||||
|
||||
import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from '@/types/vue-fontawesome'
|
||||
|
||||
library.add(faAlignLeft)
|
||||
library.add(faAngleRight)
|
||||
library.add(faArchive)
|
||||
|
@ -136,4 +138,5 @@ library.add(faTrashAlt)
|
|||
library.add(faUser)
|
||||
library.add(faUsers)
|
||||
|
||||
export default FontAwesomeIcon
|
||||
// overwriting the wrong types
|
||||
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes
|
|
@ -35,6 +35,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
defineProps({
|
||||
|
@ -51,7 +54,7 @@ defineProps({
|
|||
default: false,
|
||||
},
|
||||
closeIcon: {
|
||||
type: String,
|
||||
type: String as PropType<IconProp>,
|
||||
default: 'times',
|
||||
},
|
||||
shadow: {
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Color } from 'csstype'
|
||||
import type { DataType } from 'csstype'
|
||||
|
||||
defineProps< {
|
||||
color: Color,
|
||||
color: DataType.Color,
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
|
|
@ -46,6 +46,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
|
@ -55,7 +58,7 @@ defineProps({
|
|||
type: String,
|
||||
},
|
||||
primaryIcon: {
|
||||
type: String,
|
||||
type: String as PropType<IconProp>,
|
||||
default: 'plus',
|
||||
},
|
||||
primaryDisabled: {
|
||||
|
|
|
@ -1,46 +1,24 @@
|
|||
<template>
|
||||
<component
|
||||
:is="componentNodeName"
|
||||
v-bind="elementBindings"
|
||||
:to="to"
|
||||
class="dropdown-item">
|
||||
<BaseButton class="dropdown-item">
|
||||
<span class="icon" v-if="icon">
|
||||
<icon :icon="icon"/>
|
||||
<Icon :icon="icon"/>
|
||||
</span>
|
||||
<span>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</span>
|
||||
</component>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, useAttrs, watchEffect} from 'vue'
|
||||
import BaseButton, { type BaseButtonProps } from '@/components/base//BaseButton.vue'
|
||||
import Icon from '@/components/misc/Icon'
|
||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
const props = defineProps<{
|
||||
to?: object,
|
||||
icon?: string | string[],
|
||||
}>()
|
||||
export interface DropDownItemProps extends BaseButtonProps {
|
||||
icon?: IconProp,
|
||||
}
|
||||
|
||||
const componentNodeName = ref<Node['nodeName']>('a')
|
||||
const elementBindings = ref({})
|
||||
|
||||
const attrs = useAttrs()
|
||||
watchEffect(() => {
|
||||
let nodeName = 'a'
|
||||
|
||||
if (props.to) {
|
||||
nodeName = 'router-link'
|
||||
}
|
||||
|
||||
if ('href' in attrs) {
|
||||
nodeName = 'BaseButton'
|
||||
}
|
||||
|
||||
componentNodeName.value = nodeName
|
||||
elementBindings.value = {
|
||||
...attrs,
|
||||
}
|
||||
})
|
||||
defineProps<DropDownItemProps>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -91,5 +69,4 @@ button.dropdown-item {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -17,14 +17,15 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {ref, type PropType} from 'vue'
|
||||
import {onClickOutside} from '@vueuse/core'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
defineProps({
|
||||
triggerIcon: {
|
||||
type: String,
|
||||
type: String as PropType<IconProp>,
|
||||
default: 'ellipsis-h',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
</message>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Message from '@/components/misc/message.vue'
|
||||
import ButtonLink from '@/components/misc/ButtonLink.vue'
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
<template>
|
||||
<input
|
||||
type="text"
|
||||
data-input
|
||||
:disabled="disabled"
|
||||
v-bind="attrs"
|
||||
ref="root"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import flatpickr from 'flatpickr'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
// FIXME: Not sure how to alias these correctly
|
||||
// import Options = Flatpickr.Options doesn't work
|
||||
type Hook = flatpickr.Options.Hook
|
||||
type HookKey = flatpickr.Options.HookKey
|
||||
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()
|
||||
}
|
||||
|
||||
function arrayify<T = unknown>(obj: T) {
|
||||
return obj instanceof Array
|
||||
? obj
|
||||
: [obj]
|
||||
}
|
||||
|
||||
function nullify<T = unknown>(value: T) {
|
||||
return (value && (value as unknown[]).length)
|
||||
? value
|
||||
: null
|
||||
}
|
||||
|
||||
// Events to emit, copied from flatpickr source
|
||||
const includedEvents = [
|
||||
'onChange',
|
||||
'onClose',
|
||||
'onDestroy',
|
||||
'onMonthChange',
|
||||
'onOpen',
|
||||
'onYearChange',
|
||||
] as HookKey[]
|
||||
|
||||
// Let's not emit these events by default
|
||||
const excludedEvents = [
|
||||
'onValueUpdate',
|
||||
'onDayCreate',
|
||||
'onParseConfig',
|
||||
'onReady',
|
||||
'onPreCalendarPosition',
|
||||
'onKeyDown',
|
||||
] as HookKey[]
|
||||
|
||||
// Keep a copy of all events for later use
|
||||
const allEvents = includedEvents.concat(excludedEvents)
|
||||
|
||||
export default {inheritAttrs: false}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect, type PropType} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Date, Array] as PropType<DateOption | DateOption[]>,
|
||||
default: null,
|
||||
required: true,
|
||||
// validator(value) {
|
||||
// return (
|
||||
// value === null ||
|
||||
// value instanceof Date ||
|
||||
// typeof value === 'string' ||
|
||||
// value instanceof String ||
|
||||
// value instanceof Array ||
|
||||
// typeof value === 'number'
|
||||
// );
|
||||
// }
|
||||
},
|
||||
// https://flatpickr.js.org/options/
|
||||
config: {
|
||||
type: Object as PropType<Options>,
|
||||
default: () => ({
|
||||
defaultDate: null,
|
||||
wrap: false,
|
||||
}),
|
||||
},
|
||||
events: {
|
||||
type: Array as PropType<HookKey[]>,
|
||||
default: () => includedEvents,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'blur',
|
||||
'update:modelValue',
|
||||
...allEvents.map(camelToKebab),
|
||||
])
|
||||
|
||||
const {modelValue, config, disabled} = toRefs(props)
|
||||
|
||||
// bind listener like onBlur
|
||||
const attrs = useAttrs()
|
||||
|
||||
const root = ref<HTMLInputElement | null>(null)
|
||||
const fp = ref<flatpickr.Instance | null>(null)
|
||||
const safeConfig = ref<Options>({ ...props.config })
|
||||
|
||||
function prepareConfig() {
|
||||
// Don't mutate original object on parent component
|
||||
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,
|
||||
localCallback,
|
||||
)
|
||||
})
|
||||
|
||||
// Watch for value changed by date-picker itself and notify parent component
|
||||
const onChange: Hook = (dates) => emit('update:modelValue', dates)
|
||||
newConfig['onChange'] = arrayify(newConfig['onChange'] || []).concat(onChange)
|
||||
|
||||
// Flatpickr does not emit input event in some cases
|
||||
// const onClose: Hook = (_selectedDates, dateStr) => emit('update:modelValue', dateStr)
|
||||
// newConfig['onClose'] = arrayify(newConfig['onClose'] || []).concat(onClose)
|
||||
|
||||
// Set initial date without emitting any event
|
||||
newConfig.defaultDate = props.modelValue || newConfig.defaultDate
|
||||
|
||||
safeConfig.value = newConfig
|
||||
|
||||
return safeConfig.value
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (
|
||||
fp.value || // Return early if flatpickr is already loaded
|
||||
!root.value // our input needs to be mounted
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
prepareConfig()
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
// Init flatpickr
|
||||
fp.value = flatpickr(element, safeConfig.value)
|
||||
})
|
||||
onBeforeUnmount(() => fp.value?.destroy())
|
||||
|
||||
watch(config, () => {
|
||||
if (!fp.value) return
|
||||
// Workaround: Don't pass hooks to configs again otherwise
|
||||
// previously registered hooks will stop working
|
||||
// Notice: we are looping through all events
|
||||
// This also means that new callbacks can not be passed once component has been initialized
|
||||
allEvents.forEach((hook) => {
|
||||
delete safeConfig.value?.[hook]
|
||||
})
|
||||
fp.value.set(safeConfig.value)
|
||||
|
||||
// Passing these properties in `set()` method will cause flatpickr to trigger some callbacks
|
||||
const configCallbacks = ['locale', 'showMonths'] as (keyof Options)[]
|
||||
|
||||
// Workaround: Allow to change locale dynamically
|
||||
configCallbacks.forEach(name => {
|
||||
if (typeof safeConfig.value?.[name] !== 'undefined' && fp.value) {
|
||||
fp.value.set(name, safeConfig.value[name])
|
||||
}
|
||||
})
|
||||
}, {deep:true})
|
||||
|
||||
const fpInput = computed(() => {
|
||||
if (!fp.value) return
|
||||
return fp.value.altInput || fp.value.input
|
||||
})
|
||||
|
||||
/**
|
||||
* init blur event
|
||||
* (is required by many validation libraries)
|
||||
*/
|
||||
function onBlur(event: Event) {
|
||||
emit('blur', nullify((event.target as HTMLInputElement).value))
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
watchEffect(() => {
|
||||
if (disabled.value) {
|
||||
fpInput.value?.setAttribute('disabled', '')
|
||||
} else {
|
||||
fpInput.value?.removeAttribute('disabled')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Watch for changes from parent component and update DOM
|
||||
*/
|
||||
watch(
|
||||
modelValue,
|
||||
newValue => {
|
||||
// Prevent updates if v-model value is same as input's current value
|
||||
if (!root.value || newValue === nullify(root.value.value)) return
|
||||
// Make sure we have a flatpickr instance and
|
||||
// notify flatpickr instance that there is a change in value
|
||||
fp.value?.setDate(newValue, true)
|
||||
},
|
||||
{deep: true},
|
||||
)
|
||||
</script>
|
|
@ -2,6 +2,12 @@
|
|||
<div class="loader-container is-loading"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.loader-container {
|
||||
height: 100%;
|
||||
|
|
|
@ -99,6 +99,9 @@ watchEffect(() => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$modal-margin: 4rem;
|
||||
$modal-width: 1024px;
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
z-index: 4000;
|
||||
|
@ -147,16 +150,16 @@ watchEffect(() => {
|
|||
// scrolling-content
|
||||
// used e.g. for <TaskDetailViewModal>
|
||||
.scrolling .modal-content {
|
||||
max-width: 1024px;
|
||||
max-width: $modal-width;
|
||||
width: 100%;
|
||||
margin: 4rem auto;
|
||||
margin: $modal-margin auto;
|
||||
|
||||
max-height: none; // reset bulma
|
||||
overflow: visible; // reset bulma
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
max-height: none; // reset bulma
|
||||
margin: 4rem auto; // reset bulma
|
||||
margin: $modal-margin auto; // reset bulma
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -189,14 +192,23 @@ watchEffect(() => {
|
|||
}
|
||||
|
||||
.close {
|
||||
$close-button-min-space: 84px;
|
||||
$close-button-padding: 26px;
|
||||
position: fixed;
|
||||
top: 5px;
|
||||
right: 26px;
|
||||
color: var(--white);
|
||||
right: $close-button-padding;
|
||||
color: var(--grey-900);
|
||||
font-size: 2rem;
|
||||
|
||||
@media screen and (max-width: $desktop) {
|
||||
color: var(--grey-900);
|
||||
|
||||
@media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
|
||||
top: calc(5px + $modal-margin);
|
||||
right: 50%;
|
||||
// we align the close button to the modal until there is enough space outside for it
|
||||
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
|
||||
}
|
||||
// we can only use light color when there is enough space for the close button next to the modal
|
||||
@media screen and (min-width: calc(#{$desktop } + #{$close-button-min-space})) {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -16,11 +16,7 @@
|
|||
</div>
|
||||
<div
|
||||
class="buttons is-right"
|
||||
v-if="
|
||||
item.data &&
|
||||
item.data.actions &&
|
||||
item.data.actions.length > 0
|
||||
"
|
||||
v-if="item.data?.actions?.length > 0"
|
||||
>
|
||||
<x-button
|
||||
:key="'action_' + i"
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<slot name="trigger" :isOpen="open" :toggle="toggle"></slot>
|
||||
<div class="popup" :class="{'is-open': open, 'has-overflow': props.hasOverflow && open}" ref="popup">
|
||||
<div
|
||||
class="popup"
|
||||
:class="{
|
||||
'is-open': open,
|
||||
'has-overflow': props.hasOverflow && open
|
||||
}"
|
||||
ref="popup"
|
||||
>
|
||||
<slot name="content" :isOpen="open"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {onBeforeUnmount, onMounted, ref} from 'vue'
|
||||
|
||||
const open = ref(false)
|
||||
const popup = ref(null)
|
||||
|
||||
const toggle = () => {
|
||||
open.value = !open.value
|
||||
}
|
||||
import {ref} from 'vue'
|
||||
import {onClickOutside} from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
hasOverflow: {
|
||||
|
@ -23,24 +23,22 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
function hidePopup(e) {
|
||||
const open = ref(false)
|
||||
const popup = ref<HTMLElement | null>(null)
|
||||
|
||||
function close() {
|
||||
open.value = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
onClickOutside(popup, () => {
|
||||
if (!open.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// we actually want to use popup.$el, not its value.
|
||||
// eslint-disable-next-line vue/no-ref-as-operand
|
||||
closeWhenClickedOutside(e, popup.value, () => {
|
||||
open.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', hidePopup)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', hidePopup)
|
||||
close()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
v-else-if="type === 'dropdown'"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:class="{'is-disabled': disabled}"
|
||||
:disabled="disabled"
|
||||
:icon="iconName"
|
||||
>
|
||||
{{ buttonText }}
|
||||
|
@ -44,6 +44,7 @@ import SubscriptionModel from '@/models/subscription'
|
|||
import type {ISubscription} from '@/modelTypes/ISubscription'
|
||||
|
||||
import {success} from '@/message'
|
||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
const props = defineProps({
|
||||
entity: String,
|
||||
|
@ -104,7 +105,7 @@ const tooltipText = computed(() => {
|
|||
})
|
||||
|
||||
const buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
|
||||
const iconName = computed(() => props.modelValue ? ['far', 'bell-slash'] : 'bell')
|
||||
const iconName = computed<IconProp>(() => props.modelValue ? ['far', 'bell-slash'] : 'bell')
|
||||
const disabled = computed(() => props.modelValue && subscriptionEntity.value !== props.entity)
|
||||
|
||||
function changeSubscription() {
|
||||
|
|
|
@ -79,11 +79,9 @@ onMounted(() => {
|
|||
|
||||
function setSubscriptionInStore(sub: ISubscription) {
|
||||
subscription.value = sub
|
||||
namespaceStore.setNamespaces([
|
||||
{
|
||||
...props.namespace,
|
||||
subscription: sub,
|
||||
},
|
||||
])
|
||||
namespaceStore.setNamespaceById({
|
||||
...props.namespace,
|
||||
subscription: sub,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -76,7 +76,7 @@ const notifications = computed(() => {
|
|||
})
|
||||
const userInfo = computed(() => authStore.info)
|
||||
|
||||
let interval: number
|
||||
let interval: ReturnType<typeof setInterval>
|
||||
|
||||
onMounted(() => {
|
||||
loadNotifications()
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,255 @@
|
|||
<template>
|
||||
<Loading
|
||||
v-if="props.isLoading && tasks.size || dayjsLanguageLoading"
|
||||
class="gantt-container"
|
||||
/>
|
||||
<div class="gantt-container" v-else>
|
||||
<GGanttChart
|
||||
:date-format="DAYJS_ISO_DATE_FORMAT"
|
||||
:chart-start="isoToKebabDate(filters.dateFrom)"
|
||||
:chart-end="isoToKebabDate(filters.dateTo)"
|
||||
precision="day"
|
||||
bar-start="startDate"
|
||||
bar-end="endDate"
|
||||
:grid="true"
|
||||
@dragend-bar="updateGanttTask"
|
||||
@dblclick-bar="openTask"
|
||||
:width="ganttChartWidth + 'px'"
|
||||
>
|
||||
<template #timeunit="{label, value}">
|
||||
<div
|
||||
class="timeunit-wrapper"
|
||||
:class="{'today': dayIsToday(label)}"
|
||||
>
|
||||
<span>{{ value }}</span>
|
||||
<span class="weekday">
|
||||
{{ weekdayFromTimeLabel(label) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<GGanttRow
|
||||
v-for="(bar, k) in ganttBars"
|
||||
:key="k"
|
||||
label=""
|
||||
:bars="bar"
|
||||
/>
|
||||
</GGanttChart>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch, toRefs} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {format, parse} from 'date-fns'
|
||||
import dayjs from 'dayjs'
|
||||
import isToday from 'dayjs/plugin/isToday'
|
||||
|
||||
import {getHexColor} from '@/models/task'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
import {isoToKebabDate} from '@/helpers/time/isoToKebabDate'
|
||||
import {parseKebabDate} from '@/helpers/time/parseKebabDate'
|
||||
|
||||
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
|
||||
import type {DateISO} from '@/types/DateISO'
|
||||
import type {GanttFilters} from '@/views/list/helpers/useGanttFilters'
|
||||
|
||||
import {
|
||||
extendDayjs,
|
||||
GGanttChart,
|
||||
GGanttRow,
|
||||
type GanttBarObject,
|
||||
} from '@infectoone/vue-ganttastic'
|
||||
|
||||
import Loading from '@/components/misc/loading.vue'
|
||||
import {MILLISECONDS_A_DAY} from '@/constants/date'
|
||||
|
||||
export interface GanttChartProps {
|
||||
isLoading: boolean,
|
||||
filters: GanttFilters,
|
||||
tasks: Map<ITask['id'], ITask>,
|
||||
defaultTaskStartDate: DateISO
|
||||
defaultTaskEndDate: DateISO
|
||||
}
|
||||
|
||||
const DAYJS_ISO_DATE_FORMAT = 'YYYY-MM-DD'
|
||||
|
||||
const props = defineProps<GanttChartProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:task', task: ITaskPartialWithId): void
|
||||
}>()
|
||||
|
||||
const {tasks, filters} = toRefs(props)
|
||||
|
||||
// setup dayjs for vue-ganttastic
|
||||
const dayjsLanguageLoading = ref(false)
|
||||
// const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
|
||||
dayjs.extend(isToday)
|
||||
extendDayjs()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const dateFromDate = computed(() => new Date(new Date(filters.value.dateFrom).setHours(0,0,0,0)))
|
||||
const dateToDate = computed(() => new Date(new Date(filters.value.dateTo).setHours(23,59,0,0)))
|
||||
|
||||
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 ganttBars = ref<GanttBarObject[][]>([])
|
||||
|
||||
/**
|
||||
* Update ganttBars when tasks change
|
||||
*/
|
||||
watch(
|
||||
tasks,
|
||||
() => {
|
||||
ganttBars.value = []
|
||||
tasks.value.forEach(t => ganttBars.value.push(transformTaskToGanttBar(t)))
|
||||
},
|
||||
{deep: true, immediate: true},
|
||||
)
|
||||
|
||||
function transformTaskToGanttBar(t: ITask) {
|
||||
const black = 'var(--grey-800)'
|
||||
return [{
|
||||
startDate: isoToKebabDate(t.startDate ? t.startDate.toISOString() : props.defaultTaskStartDate),
|
||||
endDate: isoToKebabDate(t.endDate ? t.endDate.toISOString() : props.defaultTaskEndDate),
|
||||
ganttBarConfig: {
|
||||
id: String(t.id),
|
||||
label: t.title,
|
||||
hasHandles: true,
|
||||
style: {
|
||||
color: t.startDate ? (colorIsDark(getHexColor(t.hexColor)) ? black : 'white') : black,
|
||||
backgroundColor: t.startDate ? getHexColor(t.hexColor) : 'var(--grey-100)',
|
||||
border: t.startDate ? '' : '2px dashed var(--grey-300)',
|
||||
'text-decoration': t.done ? 'line-through' : null,
|
||||
},
|
||||
},
|
||||
} as GanttBarObject]
|
||||
}
|
||||
|
||||
async function updateGanttTask(e: {
|
||||
bar: GanttBarObject;
|
||||
e: MouseEvent;
|
||||
datetime?: string | undefined;
|
||||
}) {
|
||||
emit('update:task', {
|
||||
id: Number(e.bar.ganttBarConfig.id),
|
||||
startDate: new Date(parseKebabDate(e.bar.startDate).setHours(0,0,0,0)),
|
||||
endDate: new Date(parseKebabDate(e.bar.endDate).setHours(23,59,0,0)),
|
||||
})
|
||||
}
|
||||
|
||||
function openTask(e: {
|
||||
bar: GanttBarObject;
|
||||
e: MouseEvent;
|
||||
datetime?: string | undefined;
|
||||
}) {
|
||||
router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: e.bar.ganttBarConfig.id},
|
||||
state: {backdropView: router.currentRoute.value.fullPath},
|
||||
})
|
||||
}
|
||||
|
||||
function parseTimeLabel(label: string) {
|
||||
return parse(label, 'dd.MMM', dateFromDate.value)
|
||||
}
|
||||
|
||||
function weekdayFromTimeLabel(label: string): string {
|
||||
const parsed = parseTimeLabel(label)
|
||||
return format(parsed, 'E')
|
||||
}
|
||||
|
||||
function dayIsToday(label: string): boolean {
|
||||
const parsed = parseTimeLabel(label)
|
||||
|
||||
const today = new Date()
|
||||
return parsed.getDate() === today.getDate() &&
|
||||
parsed.getMonth() === today.getMonth() &&
|
||||
parsed.getFullYear() === today.getFullYear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.gantt-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
// Not scoped because we need to style the elements inside the gantt chart component
|
||||
.g-gantt-chart {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.g-gantt-row-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.g-upper-timeunit, .g-timeunit {
|
||||
background: var(--white) !important;
|
||||
font-family: $vikunja-font;
|
||||
}
|
||||
|
||||
.g-upper-timeunit {
|
||||
font-weight: bold;
|
||||
border-right: 1px solid var(--grey-200);
|
||||
padding: .5rem 0;
|
||||
}
|
||||
|
||||
.g-timeunit .timeunit-wrapper {
|
||||
padding: 0.5rem 0;
|
||||
font-size: 1rem !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
&.today {
|
||||
background: var(--primary);
|
||||
color: var(--white);
|
||||
border-radius: 5px 5px 0 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.g-timeaxis {
|
||||
height: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.g-gantt-row > .g-gantt-row-bars-container {
|
||||
border-bottom: none !important;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.g-gantt-row:nth-child(odd) {
|
||||
background: hsla(var(--grey-100-hsl), .5);
|
||||
}
|
||||
|
||||
.g-gantt-bar {
|
||||
border-radius: $radius * 1.5;
|
||||
overflow: visible;
|
||||
font-size: .85rem;
|
||||
|
||||
&-handle-left,
|
||||
&-handle-right {
|
||||
width: 6px !important;
|
||||
height: 75% !important;
|
||||
opacity: .75 !important;
|
||||
border-radius: $radius !important;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<form
|
||||
@submit.prevent="createTask"
|
||||
class="add-new-task"
|
||||
>
|
||||
<transition name="width">
|
||||
<input
|
||||
v-if="newTaskFieldActive"
|
||||
v-model="newTaskTitle"
|
||||
@blur="hideCreateNewTask"
|
||||
@keyup.esc="newTaskFieldActive = false"
|
||||
class="input"
|
||||
ref="newTaskTitleField"
|
||||
type="text"
|
||||
/>
|
||||
</transition>
|
||||
<x-button @click="showCreateTaskOrCreate" :shadow="false" icon="plus">
|
||||
{{ $t('task.new') }}
|
||||
</x-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {nextTick, ref} from 'vue'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create-task', title: string): Promise<ITask>
|
||||
}>()
|
||||
|
||||
const newTaskFieldActive = ref(false)
|
||||
const newTaskTitleField = ref()
|
||||
const newTaskTitle = ref('')
|
||||
|
||||
function showCreateTaskOrCreate() {
|
||||
if (!newTaskFieldActive.value) {
|
||||
// Timeout to not send the form if the field isn't even shown
|
||||
setTimeout(() => {
|
||||
newTaskFieldActive.value = true
|
||||
nextTick(() => newTaskTitleField.value.focus())
|
||||
}, 100)
|
||||
} else {
|
||||
createTask()
|
||||
}
|
||||
}
|
||||
|
||||
function hideCreateNewTask() {
|
||||
if (newTaskTitle.value === '') {
|
||||
nextTick(() => (newTaskFieldActive.value = false))
|
||||
}
|
||||
}
|
||||
|
||||
async function createTask() {
|
||||
if (!newTaskFieldActive.value) {
|
||||
return
|
||||
}
|
||||
await emit('create-task', newTaskTitle.value)
|
||||
newTaskTitle.value = ''
|
||||
hideCreateNewTask()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.add-new-task {
|
||||
padding: 1rem .7rem .4rem .7rem;
|
||||
display: flex;
|
||||
max-width: 450px;
|
||||
|
||||
.input {
|
||||
margin-right: .7rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: .68rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -214,7 +214,7 @@ async function addTask() {
|
|||
return rel
|
||||
})
|
||||
await Promise.all(relations)
|
||||
} catch (e: { message?: string }) {
|
||||
} catch (e: any) {
|
||||
newTaskTitle.value = taskTitleBackup
|
||||
if (e?.message === 'NO_LIST') {
|
||||
errorMessage.value = t('list.create.addListRequired')
|
||||
|
|
|
@ -1,642 +0,0 @@
|
|||
<template>
|
||||
<div class="gantt-chart">
|
||||
<div class="filter-container">
|
||||
<div class="items">
|
||||
<filter-popup
|
||||
v-model="params"
|
||||
@update:modelValue="loadTasks()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dates">
|
||||
<template v-for="(y, yk) in days" :key="yk + 'year'">
|
||||
<div class="months">
|
||||
<div
|
||||
:key="mk + 'month'"
|
||||
class="month"
|
||||
v-for="(m, mk) in days[yk]"
|
||||
>
|
||||
{{ formatMonthAndYear(yk, parseInt(mk) + 1) }}
|
||||
<div class="days">
|
||||
<div
|
||||
:class="{ today: d.toDateString() === now.toDateString() }"
|
||||
:key="dk + 'day'"
|
||||
:style="{ width: dayWidth + 'px' }"
|
||||
class="day"
|
||||
v-for="(d, dk) in days[yk][mk]"
|
||||
>
|
||||
<span class="theday" v-if="dayWidth > 25">
|
||||
{{ d.getDate() }}
|
||||
</span>
|
||||
<span class="weekday" v-if="dayWidth > 25">
|
||||
{{
|
||||
d.toLocaleString('en-us', {
|
||||
weekday: 'short',
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div :style="{ width: fullWidth + 'px' }" class="tasks">
|
||||
<div
|
||||
v-for="(t, k) in theTasks"
|
||||
:key="t ? t.id : 0"
|
||||
:style="{
|
||||
background:
|
||||
'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' +
|
||||
(k % 2 === 0
|
||||
? '#fafafa 1px, #fafafa '
|
||||
: '#fff 1px, #fff ') +
|
||||
dayWidth +
|
||||
'px)',
|
||||
}"
|
||||
class="row"
|
||||
>
|
||||
<VueDragResize
|
||||
:class="{
|
||||
done: t ? t.done : false,
|
||||
'is-current-edit': taskToEdit !== null && taskToEdit.id === t.id,
|
||||
'has-light-text': !colorIsDark(t.getHexColor()),
|
||||
'has-dark-text': colorIsDark(t.getHexColor()),
|
||||
}"
|
||||
:gridX="dayWidth"
|
||||
:h="31"
|
||||
:isActive="canWrite"
|
||||
:minw="dayWidth"
|
||||
:parentLimitation="true"
|
||||
:parentW="fullWidth"
|
||||
:snapToGrid="true"
|
||||
:sticks="['mr', 'ml']"
|
||||
:style="{
|
||||
'border-color': t.getHexColor(),
|
||||
'background-color': t.getHexColor(),
|
||||
}"
|
||||
:w="t.durationDays * dayWidth"
|
||||
:x="t.offsetDays * dayWidth - 6"
|
||||
:y="0"
|
||||
@dragstop="(e) => resizeTask(t, e)"
|
||||
@resizestop="(e) => resizeTask(t, e)"
|
||||
axis="x"
|
||||
class="task"
|
||||
>
|
||||
<span
|
||||
:class="{
|
||||
'has-high-priority': t.priority >= priorities.HIGH,
|
||||
'has-not-so-high-priority':
|
||||
t.priority === priorities.HIGH,
|
||||
'has-super-high-priority':
|
||||
t.priority === priorities.DO_NOW,
|
||||
}"
|
||||
>
|
||||
{{ t.title }}
|
||||
</span>
|
||||
<priority-label :priority="t.priority" :done="t.done"/>
|
||||
<!-- using the key here forces vue to use the updated version model and not the response returned by the api -->
|
||||
<!-- FIXME: add label -->
|
||||
<BaseButton @click="editTask(theTasks[k])" class="edit-toggle">
|
||||
<icon icon="pen"/>
|
||||
</BaseButton>
|
||||
</VueDragResize>
|
||||
</div>
|
||||
<template v-if="showTaskswithoutDates">
|
||||
<div
|
||||
:key="t.id"
|
||||
:style="{
|
||||
background:
|
||||
'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' +
|
||||
(k % 2 === 0
|
||||
? '#fafafa 1px, #fafafa '
|
||||
: '#fff 1px, #fff ') +
|
||||
dayWidth +
|
||||
'px)',
|
||||
}"
|
||||
class="row"
|
||||
v-for="(t, k) in tasksWithoutDates"
|
||||
>
|
||||
<VueDragResize
|
||||
:gridX="dayWidth"
|
||||
:h="31"
|
||||
:isActive="canWrite"
|
||||
:minw="dayWidth"
|
||||
:parentLimitation="true"
|
||||
:parentW="fullWidth"
|
||||
:snapToGrid="true"
|
||||
:sticks="['mr', 'ml']"
|
||||
:x="dayOffsetUntilToday * dayWidth - 6"
|
||||
:y="0"
|
||||
@dragstop="(e) => resizeTask(t, e)"
|
||||
@resizestop="(e) => resizeTask(t, e)"
|
||||
axis="x"
|
||||
class="task nodate"
|
||||
v-tooltip="$t('list.gantt.noDates')"
|
||||
>
|
||||
<span>{{ t.title }}</span>
|
||||
</VueDragResize>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<form
|
||||
@submit.prevent="addNewTask()"
|
||||
class="add-new-task"
|
||||
v-if="canWrite"
|
||||
>
|
||||
<transition name="width">
|
||||
<input
|
||||
@blur="hideCrateNewTask"
|
||||
@keyup.esc="newTaskFieldActive = false"
|
||||
class="input"
|
||||
ref="newTaskTitleField"
|
||||
type="text"
|
||||
v-if="newTaskFieldActive"
|
||||
v-model="newTaskTitle"
|
||||
/>
|
||||
</transition>
|
||||
<x-button @click="showCreateNewTask" :shadow="false" icon="plus">
|
||||
{{ $t('list.list.newTaskCta') }}
|
||||
</x-button>
|
||||
</form>
|
||||
<transition name="fade">
|
||||
<edit-task
|
||||
v-if="isTaskEdit"
|
||||
class="taskedit"
|
||||
:title="$t('list.list.editTask')"
|
||||
@close="() => {isTaskEdit = false;taskToEdit = null}"
|
||||
:task="taskToEdit"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
import {mapState} from 'pinia'
|
||||
|
||||
import VueDragResize from 'vue-drag-resize'
|
||||
import EditTask from './edit-task.vue'
|
||||
|
||||
import TaskService from '../../services/task'
|
||||
import TaskModel from '../../models/task'
|
||||
import {PRIORITIES as priorities} from '@/constants/priorities'
|
||||
import PriorityLabel from './partials/priorityLabel.vue'
|
||||
import TaskCollectionService from '../../services/taskCollection'
|
||||
import {RIGHTS as Rights} from '@/constants/rights'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
import {formatDate} from '@/helpers/time/formatDate'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'GanttChart',
|
||||
components: {
|
||||
BaseButton,
|
||||
FilterPopup,
|
||||
PriorityLabel,
|
||||
EditTask,
|
||||
VueDragResize,
|
||||
},
|
||||
props: {
|
||||
listId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
showTaskswithoutDates: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dateFrom: {
|
||||
default: () => new Date(new Date().setDate(new Date().getDate() - 15)),
|
||||
},
|
||||
dateTo: {
|
||||
default: () => new Date(new Date().setDate(new Date().getDate() + 30)),
|
||||
},
|
||||
// The width of a day in pixels, used to calculate all sorts of things.
|
||||
dayWidth: {
|
||||
type: Number,
|
||||
default: 35,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
days: [],
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
theTasks: [], // Pretty much a copy of the prop, since we cant mutate the prop directly
|
||||
tasksWithoutDates: [],
|
||||
taskService: new TaskService(),
|
||||
fullWidth: 0,
|
||||
now: new Date(),
|
||||
dayOffsetUntilToday: 0,
|
||||
isTaskEdit: false,
|
||||
taskToEdit: null,
|
||||
newTaskTitle: '',
|
||||
newTaskFieldActive: false,
|
||||
priorities: priorities,
|
||||
taskCollectionService: new TaskCollectionService(),
|
||||
|
||||
params: {
|
||||
sort_by: ['done', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dateFrom: 'buildTheGanttChart',
|
||||
dateTo: 'buildTheGanttChart',
|
||||
listId: 'parseTasks',
|
||||
},
|
||||
mounted() {
|
||||
this.buildTheGanttChart()
|
||||
},
|
||||
computed: mapState(useBaseStore, {
|
||||
canWrite: (state) => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
colorIsDark,
|
||||
buildTheGanttChart() {
|
||||
this.setDates()
|
||||
this.prepareGanttDays()
|
||||
this.parseTasks()
|
||||
},
|
||||
setDates() {
|
||||
this.startDate = new Date(this.dateFrom)
|
||||
this.endDate = new Date(this.dateTo)
|
||||
console.debug('setDates; start date: ', this.startDate, 'end date:', this.endDate, 'date from:', this.dateFrom, 'date to:', this.dateTo)
|
||||
|
||||
this.dayOffsetUntilToday = Math.floor((this.now - this.startDate) / 1000 / 60 / 60 / 24) + 1
|
||||
},
|
||||
prepareGanttDays() {
|
||||
console.debug('prepareGanttDays; start date: ', this.startDate, 'end date:', this.endDate)
|
||||
// Layout: years => [months => [days]]
|
||||
const years = {}
|
||||
for (
|
||||
let d = this.startDate;
|
||||
d <= this.endDate;
|
||||
d.setDate(d.getDate() + 1)
|
||||
) {
|
||||
const date = new Date(d)
|
||||
if (years[date.getFullYear() + ''] === undefined) {
|
||||
years[date.getFullYear() + ''] = {}
|
||||
}
|
||||
if (years[date.getFullYear() + ''][date.getMonth() + ''] === undefined) {
|
||||
years[date.getFullYear() + ''][date.getMonth() + ''] = []
|
||||
}
|
||||
years[date.getFullYear() + ''][date.getMonth() + ''].push(date)
|
||||
this.fullWidth += this.dayWidth
|
||||
}
|
||||
console.debug('prepareGanttDays; years:', years)
|
||||
this.days = years
|
||||
},
|
||||
|
||||
parseTasks() {
|
||||
this.setDates()
|
||||
this.loadTasks()
|
||||
},
|
||||
|
||||
async loadTasks() {
|
||||
this.theTasks = []
|
||||
this.tasksWithoutDates = []
|
||||
|
||||
const getAllTasks = async (page = 1) => {
|
||||
const tasks = await this.taskCollectionService.getAll({listId: this.listId}, this.params, page)
|
||||
if (page < this.taskCollectionService.totalPages) {
|
||||
const nextTasks = await getAllTasks(page + 1)
|
||||
return tasks.concat(nextTasks)
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
const tasks = await getAllTasks()
|
||||
this.theTasks = tasks
|
||||
.filter((t) => {
|
||||
if (t.startDate === null && !t.done) {
|
||||
this.tasksWithoutDates.push(t)
|
||||
}
|
||||
return (
|
||||
t.startDate >= this.startDate &&
|
||||
t.endDate <= this.endDate
|
||||
)
|
||||
})
|
||||
.map((t) => this.addGantAttributes(t))
|
||||
.sort(function (a, b) {
|
||||
if (a.startDate < b.startDate) return -1
|
||||
if (a.startDate > b.startDate) return 1
|
||||
return 0
|
||||
})
|
||||
},
|
||||
addGantAttributes(t) {
|
||||
if (typeof t.durationDays !== 'undefined' && typeof t.offsetDays !== 'undefined') {
|
||||
return t
|
||||
}
|
||||
|
||||
t.endDate === null ? this.endDate : t.endDate
|
||||
t.durationDays = Math.floor((t.endDate - t.startDate) / 1000 / 60 / 60 / 24)
|
||||
t.offsetDays = Math.floor((t.startDate - this.startDate) / 1000 / 60 / 60 / 24)
|
||||
return t
|
||||
},
|
||||
async resizeTask(taskDragged, newRect) {
|
||||
if (this.isTaskEdit) {
|
||||
return
|
||||
}
|
||||
|
||||
let newTask = {...taskDragged}
|
||||
|
||||
const didntHaveDates = newTask.startDate === null ? true : false
|
||||
|
||||
const startDate = new Date(this.startDate)
|
||||
startDate.setDate(
|
||||
startDate.getDate() + newRect.left / this.dayWidth,
|
||||
)
|
||||
startDate.setUTCHours(0)
|
||||
startDate.setUTCMinutes(0)
|
||||
startDate.setUTCSeconds(0)
|
||||
startDate.setUTCMilliseconds(0)
|
||||
newTask.startDate = startDate
|
||||
const endDate = new Date(startDate)
|
||||
endDate.setDate(
|
||||
startDate.getDate() + newRect.width / this.dayWidth,
|
||||
)
|
||||
newTask.startDate = startDate
|
||||
newTask.endDate = endDate
|
||||
|
||||
// We take the task from the overall tasks array because the one in it has bad data after it was updated once.
|
||||
// FIXME: This is a workaround. We should use a better mechanism to get the task or, even better,
|
||||
// prevent it from containing outdated Data in the first place.
|
||||
for (const tt in this.theTasks) {
|
||||
if (this.theTasks[tt].id === newTask.id) {
|
||||
newTask = this.theTasks[tt]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const ganttData = {
|
||||
endDate: newTask.endDate,
|
||||
durationDays: newTask.durationDays,
|
||||
offsetDays: newTask.offsetDays,
|
||||
}
|
||||
|
||||
const r = await this.taskService.update(newTask)
|
||||
r.endDate = ganttData.endDate
|
||||
r.durationDays = ganttData.durationDays
|
||||
r.offsetDays = ganttData.offsetDays
|
||||
|
||||
// If the task didn't have dates before, we'll update the list
|
||||
if (didntHaveDates) {
|
||||
for (const t in this.tasksWithoutDates) {
|
||||
if (this.tasksWithoutDates[t].id === r.id) {
|
||||
this.tasksWithoutDates.splice(t, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
this.theTasks.push(this.addGantAttributes(r))
|
||||
} else {
|
||||
for (const tt in this.theTasks) {
|
||||
if (this.theTasks[tt].id === r.id) {
|
||||
this.theTasks[tt] = this.addGantAttributes(r)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
editTask(task) {
|
||||
this.taskToEdit = task
|
||||
this.isTaskEdit = true
|
||||
},
|
||||
showCreateNewTask() {
|
||||
if (!this.newTaskFieldActive) {
|
||||
// Timeout to not send the form if the field isn't even shown
|
||||
setTimeout(() => {
|
||||
this.newTaskFieldActive = true
|
||||
this.$nextTick(() => this.$refs.newTaskTitleField.focus())
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
hideCrateNewTask() {
|
||||
if (this.newTaskTitle === '') {
|
||||
this.$nextTick(() => (this.newTaskFieldActive = false))
|
||||
}
|
||||
},
|
||||
async addNewTask() {
|
||||
if (!this.newTaskFieldActive) {
|
||||
return
|
||||
}
|
||||
const task = new TaskModel({
|
||||
title: this.newTaskTitle,
|
||||
listId: this.listId,
|
||||
})
|
||||
const r = await this.taskService.create(task)
|
||||
this.tasksWithoutDates.push(this.addGantAttributes(r))
|
||||
this.newTaskTitle = ''
|
||||
this.hideCrateNewTask()
|
||||
},
|
||||
formatMonthAndYear(year, month) {
|
||||
month = month < 10 ? '0' + month : month
|
||||
const date = new Date(`${year}-${month}-01`)
|
||||
return formatDate(date, 'MMMM, yyyy')
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$gantt-border: 1px solid var(--grey-200);
|
||||
$gantt-vertical-border-color: var(--grey-100);
|
||||
|
||||
.gantt-chart {
|
||||
overflow-x: auto;
|
||||
border-top: 1px solid var(--grey-200);
|
||||
|
||||
.dates {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
|
||||
.months {
|
||||
display: flex;
|
||||
|
||||
.month {
|
||||
padding: 0.5rem 0 0;
|
||||
border-right: $gantt-border;
|
||||
font-family: $vikunja-font;
|
||||
font-weight: bold;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.days {
|
||||
display: flex;
|
||||
|
||||
.day {
|
||||
padding: 0.5rem 0;
|
||||
font-weight: normal;
|
||||
|
||||
&.today {
|
||||
background: var(--primary);
|
||||
color: var(--white);
|
||||
border-radius: 5px 5px 0 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.theday {
|
||||
padding: 0 .5rem;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tasks {
|
||||
max-width: unset !important;
|
||||
border-top: $gantt-border;
|
||||
|
||||
.row {
|
||||
height: 45px;
|
||||
|
||||
.task {
|
||||
display: inline-block;
|
||||
border: 2px solid var(--primary);
|
||||
font-size: 0.85rem;
|
||||
margin: 0.5rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: grab;
|
||||
position: relative;
|
||||
height: 31px !important;
|
||||
|
||||
-webkit-touch-callout: none; // iOS Safari
|
||||
user-select: none; // Non-prefixed version
|
||||
|
||||
&.is-current-edit {
|
||||
border-color: var(--warning) !important;
|
||||
}
|
||||
|
||||
&.has-light-text {
|
||||
color: var(--grey-100);
|
||||
|
||||
&.done span:after {
|
||||
border-top: 1px solid var(--grey-100);
|
||||
}
|
||||
|
||||
.edit-toggle {
|
||||
color: var(--grey-100);
|
||||
}
|
||||
}
|
||||
|
||||
&.has-dark-text {
|
||||
color: var(--text);
|
||||
|
||||
&.done span:after {
|
||||
border-top: 1px solid var(--dark);
|
||||
}
|
||||
|
||||
.edit-toggle {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
&.done span {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 57%;
|
||||
}
|
||||
}
|
||||
|
||||
span:not(.high-priority) {
|
||||
max-width: calc(100% - 20px);
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
&.has-high-priority {
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
&.has-not-so-high-priority {
|
||||
max-width: calc(100% - 70px);
|
||||
}
|
||||
|
||||
&.has-super-high-priority {
|
||||
max-width: calc(100% - 111px);
|
||||
}
|
||||
|
||||
&.icon {
|
||||
width: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.high-priority {
|
||||
margin: 0 0 0 .5rem;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.edit-toggle {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&.nodate {
|
||||
border: 2px dashed var(--grey-300);
|
||||
background: var(--grey-100);
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taskedit {
|
||||
position: fixed;
|
||||
top: 10vh;
|
||||
right: 10vw;
|
||||
z-index: 5;
|
||||
|
||||
// FIXME: should be an option of the card, e.g. overflow
|
||||
:deep(.card-content) {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.add-new-task {
|
||||
padding: 1rem .7rem .4rem .7rem;
|
||||
display: flex;
|
||||
max-width: 450px;
|
||||
|
||||
.input {
|
||||
margin-right: .7rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: .68rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -165,7 +165,6 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
|
||||
import AttachmentService from '@/services/attachment'
|
||||
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
|
||||
import type AttachmentModel from '@/models/attachment'
|
||||
import type {IAttachment} from '@/modelTypes/IAttachment'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
|
@ -227,9 +226,9 @@ function uploadFilesToTask(files: File[] | FileList) {
|
|||
uploadFiles(attachmentService, props.task.id, files)
|
||||
}
|
||||
|
||||
const attachmentToDelete = ref<AttachmentModel | null>(null)
|
||||
const attachmentToDelete = ref<IAttachment | null>(null)
|
||||
|
||||
function setAttachmentToDelete(attachment: AttachmentModel | null) {
|
||||
function setAttachmentToDelete(attachment: IAttachment | null) {
|
||||
attachmentToDelete.value = attachment
|
||||
}
|
||||
|
||||
|
@ -250,7 +249,7 @@ async function deleteAttachment() {
|
|||
|
||||
const attachmentImageBlobUrl = ref<string | null>(null)
|
||||
|
||||
async function viewOrDownload(attachment: AttachmentModel) {
|
||||
async function viewOrDownload(attachment: IAttachment) {
|
||||
if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
|
||||
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
|
||||
} else {
|
||||
|
|
|
@ -61,8 +61,8 @@ const taskService = shallowReactive(new TaskService())
|
|||
const task = ref<ITask>()
|
||||
|
||||
// We're saving the due date seperately to prevent null errors in very short periods where the task is null.
|
||||
const dueDate = ref<Date>()
|
||||
const lastValue = ref<Date>()
|
||||
const dueDate = ref<Date | null>()
|
||||
const lastValue = ref<Date | null>()
|
||||
const changeInterval = ref<ReturnType<typeof setInterval>>()
|
||||
|
||||
watch(
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<Done class="heading__done" :is-done="task.done"/>
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="task.getHexColor()"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
class="mt-1 ml-2"
|
||||
/>
|
||||
<h1
|
||||
|
@ -48,6 +48,7 @@ import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
|
|||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {getHexColor} from '@/models/task'
|
||||
|
||||
const props = defineProps({
|
||||
task: {
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
v-model="list"
|
||||
:select-placeholder="$t('list.searchSelect')"
|
||||
>
|
||||
<template #searchResult="props">
|
||||
<span class="list-namespace-title search-result">{{ namespace(props.option.namespaceId) }} ></span>
|
||||
{{ props.option.title }}
|
||||
<template #searchResult="{option}">
|
||||
<span class="list-namespace-title search-result">{{ namespace((option as IList).namespaceId) }} ></span>
|
||||
{{ (option as IList).title }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
</template>
|
||||
|
@ -25,6 +25,7 @@ import type {IList} from '@/modelTypes/IList'
|
|||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import type { INamespace } from '@/modelTypes/INamespace'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -65,7 +66,7 @@ function select(l: IList | null) {
|
|||
emit('update:modelValue', list)
|
||||
}
|
||||
|
||||
function namespace(namespaceId: number) {
|
||||
function namespace(namespaceId: INamespace['id']) {
|
||||
const namespace = namespaceStore.getNamespaceById(namespaceId)
|
||||
return namespace !== null
|
||||
? namespace.title
|
||||
|
|
|
@ -1,30 +1,38 @@
|
|||
<template>
|
||||
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
||||
<fancycheckbox :disabled="(isArchived || disabled) && !canMarkAsDone" @change="markAsDone" v-model="task.done"/>
|
||||
<fancycheckbox
|
||||
:disabled="(isArchived || disabled) && !canMarkAsDone"
|
||||
@change="markAsDone"
|
||||
v-model="task.done"
|
||||
/>
|
||||
|
||||
<ColorBubble
|
||||
v-if="showListColor && listColor !== ''"
|
||||
:color="listColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
|
||||
<router-link
|
||||
:to="taskDetailRoute"
|
||||
:class="{ 'done': task.done}"
|
||||
class="tasktext">
|
||||
class="tasktext"
|
||||
>
|
||||
<span>
|
||||
<router-link
|
||||
v-if="showList && taskList !== null"
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
class="task-list"
|
||||
:class="{'mr-2': task.hexColor !== ''}"
|
||||
v-if="showList && getListById(task.listId) !== null"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
|
||||
{{ getListById(task.listId).title }}
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})">
|
||||
{{ taskList.title }}
|
||||
</router-link>
|
||||
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="task.getHexColor()"
|
||||
: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">
|
||||
|
@ -35,15 +43,22 @@
|
|||
{{ task.title }}
|
||||
</span>
|
||||
|
||||
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0" />
|
||||
<user
|
||||
<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"
|
||||
v-for="(a, i) in task.assignees"
|
||||
/>
|
||||
|
||||
<!-- FIXME: use popup -->
|
||||
<BaseButton
|
||||
v-if="+new Date(task.dueDate) > 0"
|
||||
class="dueDate"
|
||||
|
@ -62,7 +77,9 @@
|
|||
<transition name="fade">
|
||||
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
||||
</transition>
|
||||
|
||||
<priority-label :priority="task.priority" :done="task.done"/>
|
||||
|
||||
<span>
|
||||
<span class="list-task-icon" v-if="task.attachments.length > 0">
|
||||
<icon icon="paperclip"/>
|
||||
|
@ -74,184 +91,186 @@
|
|||
<icon icon="history"/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<checklist-summary :task="task"/>
|
||||
</router-link>
|
||||
|
||||
<progress
|
||||
class="progress is-small"
|
||||
v-if="task.percentDone > 0"
|
||||
:value="task.percentDone * 100" max="100">
|
||||
:value="task.percentDone * 100" max="100"
|
||||
>
|
||||
{{ task.percentDone * 100 }}%
|
||||
</progress>
|
||||
|
||||
<router-link
|
||||
v-if="!showList && currentList.id !== task.listId && taskList !== null"
|
||||
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
||||
class="task-list"
|
||||
v-if="!showList && currentList.id !== task.listId && getListById(task.listId) !== null"
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
|
||||
{{ getListById(task.listId).title }}
|
||||
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})"
|
||||
>
|
||||
{{ taskList.title }}
|
||||
</router-link>
|
||||
|
||||
<BaseButton
|
||||
:class="{'is-favorite': task.isFavorite}"
|
||||
@click="toggleFavorite"
|
||||
class="favorite">
|
||||
class="favorite"
|
||||
>
|
||||
<icon icon="star" v-if="task.isFavorite"/>
|
||||
<icon :icon="['far', 'star']" v-else/>
|
||||
</BaseButton>
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, type PropType} from 'vue'
|
||||
import {mapState} from 'pinia'
|
||||
<script setup lang="ts">
|
||||
import {ref, watch, shallowReactive, toRef, type PropType, onMounted, onBeforeUnmount, computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import TaskModel from '@/models/task'
|
||||
import TaskModel, { getHexColor } from '@/models/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import PriorityLabel from './priorityLabel.vue'
|
||||
import TaskService from '../../../services/task'
|
||||
import Labels from '@/components/tasks/partials/labels.vue'
|
||||
|
||||
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
||||
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 '../../input/fancycheckbox.vue'
|
||||
import DeferTask from './defer-task.vue'
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import ChecklistSummary from './checklist-summary.vue'
|
||||
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'singleTaskInList',
|
||||
data() {
|
||||
return {
|
||||
taskService: new TaskService(),
|
||||
task: new TaskModel(),
|
||||
showDefer: false,
|
||||
}
|
||||
const props = defineProps({
|
||||
theTask: {
|
||||
type: Object as PropType<ITask>,
|
||||
required: true,
|
||||
},
|
||||
components: {
|
||||
ColorBubble,
|
||||
BaseButton,
|
||||
ChecklistSummary,
|
||||
DeferTask,
|
||||
Fancycheckbox,
|
||||
User,
|
||||
Labels,
|
||||
PriorityLabel,
|
||||
isArchived: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
props: {
|
||||
theTask: {
|
||||
type: Object as PropType<ITask>,
|
||||
required: true,
|
||||
},
|
||||
isArchived: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showList: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showListColor: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
canMarkAsDone: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showList: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emits: ['task-updated'],
|
||||
watch: {
|
||||
theTask(newVal) {
|
||||
this.task = newVal
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
mounted() {
|
||||
this.task = this.theTask
|
||||
document.addEventListener('click', this.hideDeferDueDatePopup)
|
||||
showListColor: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.hideDeferDueDatePopup)
|
||||
},
|
||||
computed: {
|
||||
...mapState(useListStore, {
|
||||
getListById: 'getListById',
|
||||
}),
|
||||
listColor() {
|
||||
const list = this.getListById(this.task.listId)
|
||||
return list !== null ? list.hexColor : ''
|
||||
},
|
||||
currentList() {
|
||||
const baseStore = useBaseStore()
|
||||
return typeof baseStore.currentList === 'undefined' ? {
|
||||
id: 0,
|
||||
title: '',
|
||||
} : baseStore.currentList
|
||||
},
|
||||
taskDetailRoute() {
|
||||
return {
|
||||
name: 'task.detail',
|
||||
params: {id: this.task.id},
|
||||
// TODO: re-enable opening task detail in modal
|
||||
// state: { backdropView: this.$router.currentRoute.value.fullPath },
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatDateSince,
|
||||
formatISO,
|
||||
formatDateLong,
|
||||
|
||||
async markAsDone(checked: boolean) {
|
||||
const updateFunc = async () => {
|
||||
const task = await useTaskStore().update(this.task)
|
||||
this.task = task
|
||||
this.$emit('task-updated', task)
|
||||
this.$message.success({
|
||||
message: this.task.done ?
|
||||
this.$t('task.doneSuccess') :
|
||||
this.$t('task.undoneSuccess'),
|
||||
}, [{
|
||||
title: 'Undo',
|
||||
callback: () => this.undoDone(checked),
|
||||
}])
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
|
||||
} else {
|
||||
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
}
|
||||
},
|
||||
|
||||
undoDone(checked: boolean) {
|
||||
this.task.done = !this.task.done
|
||||
this.markAsDone(!checked)
|
||||
},
|
||||
|
||||
async toggleFavorite() {
|
||||
this.task.isFavorite = !this.task.isFavorite
|
||||
this.task = await this.taskService.update(this.task)
|
||||
this.$emit('task-updated', this.task)
|
||||
useNamespaceStore().loadNamespacesIfFavoritesDontExist()
|
||||
},
|
||||
hideDeferDueDatePopup(e) {
|
||||
if (!this.showDefer) {
|
||||
return
|
||||
}
|
||||
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
|
||||
this.showDefer = false
|
||||
})
|
||||
},
|
||||
canMarkAsDone: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['task-updated'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const task = ref<ITask>(new TaskModel())
|
||||
const showDefer = ref(false)
|
||||
|
||||
const theTask = toRef(props, 'theTask')
|
||||
|
||||
watch(
|
||||
theTask,
|
||||
newVal => {
|
||||
task.value = newVal
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
task.value = theTask.value
|
||||
document.addEventListener('click', hideDeferDueDatePopup)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', hideDeferDueDatePopup)
|
||||
})
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const listStore = useListStore()
|
||||
const taskStore = useTaskStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const taskList = computed(() => listStore.getListById(task.value.listId))
|
||||
const listColor = computed(() => taskList.value !== null ? taskList.value.hexColor : '')
|
||||
|
||||
const currentList = computed(() => {
|
||||
return typeof baseStore.currentList === 'undefined' ? {
|
||||
id: 0,
|
||||
title: '',
|
||||
} : baseStore.currentList
|
||||
})
|
||||
|
||||
const taskDetailRoute = computed(() => ({
|
||||
name: 'task.detail',
|
||||
params: {id: task.value.id},
|
||||
// TODO: re-enable opening task detail in modal
|
||||
// state: { backdropView: router.currentRoute.value.fullPath },
|
||||
}))
|
||||
|
||||
|
||||
async function markAsDone(checked: boolean) {
|
||||
const updateFunc = async () => {
|
||||
const newTask = await taskStore.update(task.value)
|
||||
task.value = newTask
|
||||
emit('task-updated', newTask)
|
||||
success({
|
||||
message: task.value.done ?
|
||||
t('task.doneSuccess') :
|
||||
t('task.undoneSuccess'),
|
||||
}, [{
|
||||
title: 'Undo',
|
||||
callback: () => undoDone(checked),
|
||||
}])
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
|
||||
} else {
|
||||
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
}
|
||||
}
|
||||
|
||||
function undoDone(checked: boolean) {
|
||||
task.value.done = !task.value.done
|
||||
markAsDone(!checked)
|
||||
}
|
||||
|
||||
async function toggleFavorite() {
|
||||
task.value.isFavorite = !task.value.isFavorite
|
||||
task.value = await taskService.update(task.value)
|
||||
emit('task-updated', task.value)
|
||||
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||
}
|
||||
|
||||
const deferDueDate = ref<typeof DeferTask | null>(null)
|
||||
function hideDeferDueDatePopup(e) {
|
||||
if (!showDefer.value) {
|
||||
return
|
||||
}
|
||||
closeWhenClickedOutside(e, deferDueDate.value.$el, () => {
|
||||
showDefer.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -3,6 +3,9 @@ import {useRouter} from 'vue-router'
|
|||
import {useEventListener} from '@vueuse/core'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {MILLISECONDS_A_HOUR, SECONDS_A_HOUR} from '@/constants/date'
|
||||
|
||||
const SECONDS_TOKEN_VALID = 60 * SECONDS_A_HOUR
|
||||
|
||||
export function useRenewTokenOnFocus() {
|
||||
const router = useRouter()
|
||||
|
@ -21,7 +24,7 @@ export function useRenewTokenOnFocus() {
|
|||
return
|
||||
}
|
||||
|
||||
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - +new Date() / 1000
|
||||
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - new Date().valueOf() / MILLISECONDS_A_HOUR
|
||||
|
||||
// If the token expiry is negative, it is already expired and we have no choice but to redirect
|
||||
// the user to the login page
|
||||
|
@ -32,7 +35,7 @@ export function useRenewTokenOnFocus() {
|
|||
}
|
||||
|
||||
// Check if the token is valid for less than 60 hours and renew if thats the case
|
||||
if (expiresIn < 60 * 3600) {
|
||||
if (expiresIn < SECONDS_TOKEN_VALID) {
|
||||
authStore.renewToken()
|
||||
console.debug('renewed token')
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import {computed, ref, watch, type Ref} from 'vue'
|
||||
import {useRouter, type RouteLocationNormalized, type RouteLocationRaw} from 'vue-router'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import equal from 'fast-deep-equal/es6'
|
||||
|
||||
export type Filters = Record<string, any>
|
||||
|
||||
export function useRouteFilters<CurrentFilters extends Filters>(
|
||||
route: Ref<RouteLocationNormalized>,
|
||||
getDefaultFilters: (route: RouteLocationNormalized) => CurrentFilters,
|
||||
routeToFilters: (route: RouteLocationNormalized) => CurrentFilters,
|
||||
filtersToRoute: (filters: CurrentFilters) => RouteLocationRaw,
|
||||
) {
|
||||
const router = useRouter()
|
||||
|
||||
const filters = ref<CurrentFilters>(routeToFilters(route.value))
|
||||
|
||||
const routeFromFiltersFullPath = computed(() => router.resolve(filtersToRoute(filters.value)).fullPath)
|
||||
|
||||
watch(() => cloneDeep(route.value), (route, oldRoute) => {
|
||||
if (
|
||||
route.name !== oldRoute.name ||
|
||||
routeFromFiltersFullPath.value === route.fullPath
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
filters.value = routeToFilters(route)
|
||||
})
|
||||
|
||||
watch(
|
||||
filters,
|
||||
async () => {
|
||||
if (routeFromFiltersFullPath.value !== route.value.fullPath) {
|
||||
await router.push(routeFromFiltersFullPath.value)
|
||||
}
|
||||
},
|
||||
// only apply new route after all filters have changed in component cycle
|
||||
{flush: 'post'},
|
||||
)
|
||||
|
||||
const hasDefaultFilters = computed(() => {
|
||||
return equal(filters.value, getDefaultFilters(route.value))
|
||||
})
|
||||
|
||||
function setDefaultFilters() {
|
||||
filters.value = getDefaultFilters(route.value)
|
||||
}
|
||||
|
||||
return {
|
||||
filters,
|
||||
hasDefaultFilters,
|
||||
setDefaultFilters,
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ export function useRouteWithModal() {
|
|||
return
|
||||
}
|
||||
|
||||
// logic from vue-router
|
||||
// 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
|
||||
|
@ -28,7 +28,9 @@ export function useRouteWithModal() {
|
|||
: typeof routePropsOption === 'function'
|
||||
? routePropsOption(route)
|
||||
: routePropsOption
|
||||
: null
|
||||
: {}
|
||||
|
||||
routeProps.backdropView = backdropView.value
|
||||
|
||||
const component = route.matched[0]?.components?.default
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import {ref, shallowReactive, watch, computed} from 'vue'
|
|||
import {useRoute} from 'vue-router'
|
||||
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import type { ITask } from '@/modelTypes/ITask'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||
export const getDefaultParams = () => ({
|
||||
|
@ -18,23 +18,12 @@ const SORT_BY_DEFAULT = {
|
|||
id: 'desc',
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||
*/
|
||||
export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
|
||||
const params = ref({...getDefaultParams()})
|
||||
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
|
||||
const sortBy = ref({ ...sortByDefault })
|
||||
|
||||
// This makes sure an id sort order is always sorted last.
|
||||
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
|
||||
// precedence over everything else, making any other sort columns pretty useless.
|
||||
function formatSortOrder(params) {
|
||||
function formatSortOrder(sortBy, params) {
|
||||
let hasIdFilter = false
|
||||
const sortKeys = Object.keys(sortBy.value)
|
||||
const sortKeys = Object.keys(sortBy)
|
||||
for (const s of sortKeys) {
|
||||
if (s === 'id') {
|
||||
sortKeys.splice(s, 1)
|
||||
|
@ -46,11 +35,24 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
|
|||
sortKeys.push('id')
|
||||
}
|
||||
params.sort_by = sortKeys
|
||||
params.order_by = sortKeys.map(s => sortBy.value[s])
|
||||
params.order_by = sortKeys.map(s => sortBy[s])
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||
*/
|
||||
export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
|
||||
const params = ref({...getDefaultParams()})
|
||||
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
|
||||
const sortBy = ref({ ...sortByDefault })
|
||||
|
||||
|
||||
|
||||
const getAllTasksParams = computed(() => {
|
||||
let loadParams = {...params.value}
|
||||
|
||||
|
@ -58,7 +60,7 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
|
|||
loadParams.s = search.value
|
||||
}
|
||||
|
||||
loadParams = formatSortOrder(loadParams)
|
||||
loadParams = formatSortOrder(sortBy.value, loadParams)
|
||||
|
||||
return [
|
||||
{listId: listId.value},
|
|
@ -0,0 +1,14 @@
|
|||
export const DATEFNS_DATE_FORMAT_KEBAB = 'yyyy-LL-dd'
|
||||
|
||||
export const SECONDS_A_MINUTE = 60
|
||||
export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60
|
||||
export const SECONDS_A_DAY = SECONDS_A_HOUR * 24
|
||||
export const SECONDS_A_WEEK = SECONDS_A_DAY * 7
|
||||
export const SECONDS_A_MONTH = SECONDS_A_DAY * 30
|
||||
export const SECONDS_A_YEAR = SECONDS_A_DAY * 365
|
||||
|
||||
export const MILLISECONDS_A_SECOND = 1000
|
||||
export const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND
|
||||
export const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND
|
||||
export const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND
|
||||
export const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND
|
|
@ -2,7 +2,7 @@ import type {Directive} from 'vue'
|
|||
import {install, uninstall} from '@github/hotkey'
|
||||
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||
|
||||
const directive: Directive = {
|
||||
const directive = <Directive<HTMLElement,string>>{
|
||||
mounted(el, {value}) {
|
||||
if(value === '') {
|
||||
return
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {AuthenticatedHTTPFactory} from '@/http-common'
|
||||
import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
|
||||
import type {AxiosResponse} from 'axios'
|
||||
|
||||
let savedToken: string | null = null
|
||||
|
|
|
@ -3,17 +3,15 @@ import {snakeCase} from 'snake-case'
|
|||
|
||||
/**
|
||||
* Transforms field names to camel case.
|
||||
* @param object
|
||||
* @returns {*}
|
||||
*/
|
||||
export function objectToCamelCase(object) {
|
||||
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
|
||||
if (typeof object !== 'object') {
|
||||
return object
|
||||
}
|
||||
|
||||
const parsedObject = {}
|
||||
const parsedObject: Record<string, any> = {}
|
||||
for (const m in object) {
|
||||
parsedObject[camelCase(m)] = object[m]
|
||||
|
||||
|
@ -25,7 +23,7 @@ export function objectToCamelCase(object) {
|
|||
|
||||
// Call it again for arrays
|
||||
if (Array.isArray(object[m])) {
|
||||
parsedObject[camelCase(m)] = object[m].map(o => objectToCamelCase(o))
|
||||
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
|
||||
}
|
||||
|
@ -40,17 +38,15 @@ export function objectToCamelCase(object) {
|
|||
|
||||
/**
|
||||
* Transforms field names to snake case - used before making an api request.
|
||||
* @param object
|
||||
* @returns {*}
|
||||
*/
|
||||
export function objectToSnakeCase(object) {
|
||||
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
|
||||
if (typeof object !== 'object') {
|
||||
return object
|
||||
}
|
||||
|
||||
const parsedObject = {}
|
||||
const parsedObject: Record<string, any> = {}
|
||||
for (const m in object) {
|
||||
parsedObject[snakeCase(m)] = object[m]
|
||||
|
||||
|
@ -65,7 +61,7 @@ export function objectToSnakeCase(object) {
|
|||
|
||||
// Call it again for arrays
|
||||
if (Array.isArray(object[m])) {
|
||||
parsedObject[snakeCase(m)] = object[m].map(o => objectToSnakeCase(o))
|
||||
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
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* @param rootElement
|
||||
* @param closeCallback A closure function to call when the click event happened outside of the rootElement.
|
||||
*/
|
||||
export const closeWhenClickedOutside = (event, rootElement, closeCallback) => {
|
||||
export const closeWhenClickedOutside = (event: MouseEvent, rootElement: HTMLElement, closeCallback: () => void) => {
|
||||
// We walk up the tree to see if any parent of the clicked element is the root element.
|
||||
// If it is not, we call the close callback. We're doing all this hassle to only call the
|
||||
// closing callback when a click happens outside of the rootElement.
|
||||
let parent = event.target.parentElement
|
||||
let parent = (event.target as HTMLElement)?.parentElement
|
||||
while (parent !== rootElement) {
|
||||
if (parent === null || parent.parentElement === null) {
|
||||
parent = null
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* @param color
|
||||
* @returns {string}
|
||||
*/
|
||||
export function colorFromHex(color) {
|
||||
export function colorFromHex(color: string) {
|
||||
if (color.substring(0, 1) === '#') {
|
||||
color = color.substring(1, 7)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { defineAsyncComponent, type AsyncComponentLoader, type AsyncComponentOptions, type Component, type ComponentPublicInstance } from 'vue'
|
||||
|
||||
import ErrorComponent from '@/components/misc/error.vue'
|
||||
import LoadingComponent from '@/components/misc/loading.vue'
|
||||
|
||||
const DEFAULT_TIMEOUT = 60000
|
||||
|
||||
export function createAsyncComponent<T extends Component = {
|
||||
new (): ComponentPublicInstance;
|
||||
}>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
|
||||
if (typeof source === 'function') {
|
||||
source = { loader: source }
|
||||
}
|
||||
|
||||
return defineAsyncComponent({
|
||||
...source,
|
||||
loadingComponent: LoadingComponent,
|
||||
errorComponent: ErrorComponent,
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
}
|
|
@ -35,7 +35,7 @@ export function setupMarkdownRenderer(checkboxId: string) {
|
|||
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
|
||||
},
|
||||
},
|
||||
highlight(code, language) {
|
||||
highlight(code: string, language: string) {
|
||||
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
|
||||
return hljs.highlight(code, {language: validLanguage}).value
|
||||
},
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
// https://stackoverflow.com/a/32108184/10924593
|
||||
export function objectIsEmpty(obj: Record<string, unknown>): boolean {
|
||||
return obj
|
||||
&& Object.keys(obj).length === 0
|
||||
&& Object.getPrototypeOf(obj) === Object.prototype
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* Make date objects from timestamps
|
||||
*/
|
||||
export function parseDateOrNull(date) {
|
||||
export function parseDateOrNull(date: string | Date) {
|
||||
if (date instanceof Date) {
|
||||
return date
|
||||
}
|
||||
|
||||
if ((typeof date === 'string' || date instanceof String) && !date.startsWith('0001')) {
|
||||
if ((typeof date === 'string') && !date.startsWith('0001')) {
|
||||
return new Date(date)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
// Save the current list view to local storage
|
||||
// We use local storage and not a store here to make it persistent across reloads.
|
||||
export const saveListView = (listId, routeName) => {
|
||||
import type { IList } from '@/modelTypes/IList'
|
||||
|
||||
type ListView = Record<IList['id'], string>
|
||||
|
||||
const DEFAULT_LIST_VIEW = 'list.list' as const
|
||||
|
||||
/**
|
||||
* Save the current list view to local storage
|
||||
*/
|
||||
export function saveListView(listId: IList['id'], routeName: string) {
|
||||
if (routeName.includes('settings.')) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!listId) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// We use local storage and not the store here to make it persistent across reloads.
|
||||
const savedListView = localStorage.getItem('listView')
|
||||
let savedListViewJson = false
|
||||
let savedListViewJson: ListView | false = false
|
||||
if (savedListView !== null) {
|
||||
savedListViewJson = JSON.parse(savedListView)
|
||||
savedListViewJson = JSON.parse(savedListView) as ListView
|
||||
}
|
||||
|
||||
let listView = {}
|
||||
let listView: ListView = {}
|
||||
if (savedListViewJson) {
|
||||
listView = savedListViewJson
|
||||
}
|
||||
|
@ -24,7 +32,7 @@ export const saveListView = (listId, routeName) => {
|
|||
localStorage.setItem('listView', JSON.stringify(listView))
|
||||
}
|
||||
|
||||
export const getListView = listId => {
|
||||
export const getListView = (listId: IList['id']) => {
|
||||
// Remove old stored settings
|
||||
const savedListView = localStorage.getItem('listView')
|
||||
if (savedListView !== null && savedListView.startsWith('list.')) {
|
||||
|
@ -32,13 +40,13 @@ export const getListView = listId => {
|
|||
}
|
||||
|
||||
if (!savedListView) {
|
||||
return 'list.list'
|
||||
return DEFAULT_LIST_VIEW
|
||||
}
|
||||
|
||||
const savedListViewJson = JSON.parse(savedListView)
|
||||
const savedListViewJson: ListView = JSON.parse(savedListView)
|
||||
|
||||
if (!savedListViewJson[listId]) {
|
||||
return 'list.list'
|
||||
return DEFAULT_LIST_VIEW
|
||||
}
|
||||
|
||||
return savedListViewJson[listId]
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
export function getSavedFilterIdFromListId(listId: IList['id']) {
|
||||
let filterId = listId * -1 - 1
|
||||
// FilterIds from listIds are always positive
|
||||
if (filterId < 0) {
|
||||
filterId = 0
|
||||
}
|
||||
return filterId
|
||||
}
|
||||
|
||||
export function isSavedFilter(list: IList) {
|
||||
return getSavedFilterIdFromListId(list.id) > 0
|
||||
}
|
|
@ -10,7 +10,7 @@ const days = {
|
|||
friday: 5,
|
||||
saturday: 6,
|
||||
sunday: 0,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in days) {
|
||||
test(`today on a ${n}`, () => {
|
||||
|
@ -32,7 +32,7 @@ const nextMonday = {
|
|||
friday: 3,
|
||||
saturday: 2,
|
||||
sunday: 1,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in nextMonday) {
|
||||
test(`next monday on a ${n}`, () => {
|
||||
|
@ -48,7 +48,7 @@ const thisWeekend = {
|
|||
friday: 1,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in thisWeekend) {
|
||||
test(`this weekend on a ${n}`, () => {
|
||||
|
@ -64,7 +64,7 @@ const laterThisWeek = {
|
|||
friday: 0,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in laterThisWeek) {
|
||||
test(`later this week on a ${n}`, () => {
|
||||
|
@ -80,7 +80,7 @@ const laterNextWeek = {
|
|||
friday: 7 + 0,
|
||||
saturday: 7 + 0,
|
||||
sunday: 7 + 0,
|
||||
}
|
||||
} as Record<string, number>
|
||||
|
||||
for (const n in laterNextWeek) {
|
||||
test(`later next week on a ${n} (this week)`, () => {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())) {
|
||||
type Day<T extends number = number> = T
|
||||
|
||||
export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())): Day {
|
||||
switch (dateString) {
|
||||
case 'today':
|
||||
return 0
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* @param dateString
|
||||
* @returns {Date}
|
||||
*/
|
||||
export const createDateFromString = dateString => {
|
||||
export function createDateFromString(dateString: string | Date) {
|
||||
if (dateString instanceof Date) {
|
||||
return dateString
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {format, formatDistanceToNow, formatISO as formatISOfns} from 'date-fns'
|
||||
|
||||
// FIXME: support all locales and load dynamically
|
||||
import {enGB, de, fr, ru} from 'date-fns/locale'
|
||||
|
||||
import {i18n} from '@/i18n'
|
||||
|
||||
const locales = {en: enGB, de, ch: de, fr, ru}
|
||||
|
||||
export function dateIsValid(date) {
|
||||
export function dateIsValid(date: Date | null) {
|
||||
if (date === null) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {MILLISECONDS_A_WEEK} from '@/constants/date'
|
||||
|
||||
export function getNextWeekDate(): Date {
|
||||
return new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000)
|
||||
return new Date((new Date()).getTime() + MILLISECONDS_A_WEEK)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import {format} from 'date-fns'
|
||||
import {DATEFNS_DATE_FORMAT_KEBAB} from '@/constants/date'
|
||||
import type {DateISO} from '@/types/DateISO'
|
||||
import type {DateKebab} from '@/types/DateKebab'
|
||||
|
||||
export function isoToKebabDate(isoDate: DateISO) {
|
||||
return format(new Date(isoDate), DATEFNS_DATE_FORMAT_KEBAB) as DateKebab
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export function parseBooleanProp(booleanProp: string | undefined) {
|
||||
return (booleanProp === 'false' || booleanProp === '0')
|
||||
? false
|
||||
: Boolean(booleanProp)
|
||||
}
|
|
@ -349,9 +349,7 @@ const getMonthFromText = (text: string, date: Date) => {
|
|||
const getDateFromInterval = (interval: number): Date => {
|
||||
const newDate = new Date()
|
||||
newDate.setDate(newDate.getDate() + interval)
|
||||
newDate.setHours(calculateNearestHours(newDate))
|
||||
newDate.setMinutes(0)
|
||||
newDate.setSeconds(0)
|
||||
newDate.setHours(calculateNearestHours(newDate), 0, 0)
|
||||
|
||||
return newDate
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import type {DateISO} from '@/types/DateISO'
|
||||
import type {DateKebab} from '@/types/DateKebab'
|
||||
|
||||
export function parseDateProp(kebabDate: DateKebab | undefined): string | undefined {
|
||||
try {
|
||||
|
||||
if (!kebabDate) {
|
||||
throw new Error('No value')
|
||||
}
|
||||
const dateValues = kebabDate.split('-')
|
||||
const [, monthString, dateString] = dateValues
|
||||
const [year, month, date] = dateValues.map(val => Number(val))
|
||||
const dateValuesAreValid = (
|
||||
!Number.isNaN(year) &&
|
||||
monthString.length >= 1 && monthString.length <= 2 &&
|
||||
!Number.isNaN(month) &&
|
||||
month >= 1 && month <= 12 &&
|
||||
dateString.length >= 1 && dateString.length <= 31 &&
|
||||
!Number.isNaN(date) &&
|
||||
date >= 1 && date <= 31
|
||||
)
|
||||
if (!dateValuesAreValid) {
|
||||
throw new Error('Invalid date values')
|
||||
}
|
||||
return new Date(year, month, date).toISOString() as DateISO
|
||||
} catch(e) {
|
||||
// ignore nonsense route queries
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import {parse} from 'date-fns'
|
||||
import {DATEFNS_DATE_FORMAT_KEBAB} from '@/constants/date'
|
||||
import type {DateKebab} from '@/types/DateKebab'
|
||||
|
||||
export function parseKebabDate(date: DateKebab): Date {
|
||||
return parse(date, DATEFNS_DATE_FORMAT_KEBAB, new Date())
|
||||
}
|
|
@ -1,19 +1,8 @@
|
|||
import {createI18n} from 'vue-i18n'
|
||||
import langEN from './lang/en.json'
|
||||
|
||||
export const i18n = createI18n({
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'en',
|
||||
legacy: true,
|
||||
globalInjection: true,
|
||||
allowComposition: true,
|
||||
messages: {
|
||||
en: langEN,
|
||||
},
|
||||
})
|
||||
|
||||
export const availableLanguages = {
|
||||
en: 'English',
|
||||
export const SUPPORTED_LOCALES = {
|
||||
'en': 'English',
|
||||
'de-DE': 'Deutsch',
|
||||
'de-swiss': 'Schwizertütsch',
|
||||
'ru-RU': 'Русский',
|
||||
|
@ -24,62 +13,72 @@ export const availableLanguages = {
|
|||
'pl-PL': 'Polski',
|
||||
'nl-NL': 'Nederlands',
|
||||
'pt-PT': 'Português',
|
||||
}
|
||||
'zh-CN': 'Chinese',
|
||||
} as Record<string, string>
|
||||
|
||||
const loadedLanguages = ['en'] // our default language that is preloaded
|
||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
||||
|
||||
const setI18nLanguage = (lang: string) => {
|
||||
export const DEFAULT_LANGUAGE: SupportedLocale= 'en'
|
||||
|
||||
export type ISOLanguage = string
|
||||
|
||||
export const i18n = createI18n({
|
||||
locale: DEFAULT_LANGUAGE, // set locale
|
||||
fallbackLocale: DEFAULT_LANGUAGE,
|
||||
legacy: true,
|
||||
globalInjection: true,
|
||||
allowComposition: true,
|
||||
inheritLocale: true,
|
||||
messages: {
|
||||
en: langEN,
|
||||
} as Record<SupportedLocale, any>,
|
||||
})
|
||||
|
||||
function setI18nLanguage(lang: SupportedLocale): SupportedLocale {
|
||||
i18n.global.locale = lang
|
||||
document.documentElement.lang =lang
|
||||
document.documentElement.lang = lang
|
||||
return lang
|
||||
}
|
||||
|
||||
export const loadLanguageAsync = lang => {
|
||||
export async function loadLanguageAsync(lang: SupportedLocale) {
|
||||
if (!lang) {
|
||||
throw new Error()
|
||||
}
|
||||
|
||||
// do not change language to the current one
|
||||
if (i18n.global.locale === lang) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
// If the same language
|
||||
i18n.global.locale === lang ||
|
||||
// If the language was already loaded
|
||||
loadedLanguages.includes(lang)
|
||||
) {
|
||||
return setI18nLanguage(lang)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// If the language hasn't been loaded yet
|
||||
return import(`./lang/${lang}.json`).then(
|
||||
messages => {
|
||||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
loadedLanguages.push(lang)
|
||||
return setI18nLanguage(lang)
|
||||
},
|
||||
)
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
export const getCurrentLanguage = () => {
|
||||
export function getCurrentLanguage(): SupportedLocale {
|
||||
const savedLanguage = localStorage.getItem('language')
|
||||
if (savedLanguage !== null) {
|
||||
return savedLanguage
|
||||
}
|
||||
|
||||
const browserLanguage = navigator.language || navigator.userLanguage
|
||||
const browserLanguage = navigator.language
|
||||
|
||||
for (const k in availableLanguages) {
|
||||
if (browserLanguage[k] === browserLanguage || k.startsWith(browserLanguage + '-')) {
|
||||
return k
|
||||
}
|
||||
}
|
||||
const language: SupportedLocale | undefined = Object.keys(SUPPORTED_LOCALES).find(langKey => {
|
||||
return langKey === browserLanguage || langKey.startsWith(browserLanguage + '-')
|
||||
})
|
||||
|
||||
return 'en'
|
||||
return language || DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
export const saveLanguage = (lang: string) => {
|
||||
export function saveLanguage(lang: SupportedLocale) {
|
||||
localStorage.setItem('language', lang)
|
||||
setLanguage()
|
||||
}
|
||||
|
||||
export const setLanguage = () => {
|
||||
loadLanguageAsync(getCurrentLanguage())
|
||||
}
|
||||
export function setLanguage() {
|
||||
return loadLanguageAsync(getCurrentLanguage())
|
||||
}
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "{type} byl úspěšně přidán."
|
||||
},
|
||||
"right": {
|
||||
"title": "Právo",
|
||||
"title": "Oprávnění",
|
||||
"read": "Pouze pro čtení",
|
||||
"readWrite": "Čtení a zápis",
|
||||
"admin": "Administrátor"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Výchozí",
|
||||
"month": "Měsíc",
|
||||
"day": "Den",
|
||||
"from": "Od",
|
||||
"to": "Do",
|
||||
"hour": "Hodina",
|
||||
"range": "Časové období",
|
||||
"noDates": "Tento úkol nemá nastaveno žádné datum."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Člen týmu byl úspěšně přidán.",
|
||||
"madeMember": "Člen týmu se úspěšně stal členem.",
|
||||
"madeAdmin": "Člen týmu byl úspěšně jmenován správcem.",
|
||||
"mustSelectUser": "Vyberte prosím uživatele.",
|
||||
"delete": {
|
||||
"header": "Smazat tým",
|
||||
"text1": "Jste si jisti, že chcete smazat tento tým a všechny jeho členy?",
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "{type} wurde erfolgreich hinzugefügt."
|
||||
},
|
||||
"right": {
|
||||
"title": "Berechtigungen",
|
||||
"title": "Berechtigung",
|
||||
"read": "Nur Leserechte",
|
||||
"readWrite": "Lesen & Schreiben",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Standard",
|
||||
"month": "Monat",
|
||||
"day": "Tag",
|
||||
"from": "Von",
|
||||
"to": "Bis",
|
||||
"hour": "Stunde",
|
||||
"range": "Zeitraum",
|
||||
"noDates": "Diese Aufgabe hat keine Daten definiert."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Das Teammitglied wurde erfolgreich hinzugefügt.",
|
||||
"madeMember": "Das Teammitglied ist nun ein normales Mitglied.",
|
||||
"madeAdmin": "Das Teammitglied ist nun ein Admin.",
|
||||
"mustSelectUser": "Bitte wähle eine:n Benutzer:in.",
|
||||
"delete": {
|
||||
"header": "Team löschen",
|
||||
"text1": "Bist du sicher, dass du dieses Team und alle seine Mitglieder löschen willst?",
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "De {type} isch erfolgriich hinzuegfüegt wore."
|
||||
},
|
||||
"right": {
|
||||
"title": "Rechts",
|
||||
"title": "Berechtigung",
|
||||
"read": "Nur Lese",
|
||||
"readWrite": "Lese und Schriibe",
|
||||
"admin": "Chef"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Standard",
|
||||
"month": "Monet",
|
||||
"day": "Taag",
|
||||
"from": "Vo",
|
||||
"to": "Bis",
|
||||
"hour": "Stunde",
|
||||
"range": "Zeitraum",
|
||||
"noDates": "Die Uufgab het no kei Datum gsetzt."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Teammitglied hinzugefüegt.",
|
||||
"madeMember": "Das Teammitglied ist ez es normals Mitglied.",
|
||||
"madeAdmin": "Teammitglied isch ez en Chef.",
|
||||
"mustSelectUser": "Bitte wähle eine:n Benutzer:in.",
|
||||
"delete": {
|
||||
"header": "Das Team chüble",
|
||||
"text1": "Bischder sicher, dasst wetsch da Team mit allne Mitglieder lösche?",
|
||||
|
|
|
@ -261,7 +261,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -286,8 +286,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -846,6 +846,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -861,7 +862,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "{type} ajouté."
|
||||
},
|
||||
"right": {
|
||||
"title": "Droit",
|
||||
"title": "Permission",
|
||||
"read": "Lecture seule",
|
||||
"readWrite": "Lecture et écriture",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Par défaut",
|
||||
"month": "Mois",
|
||||
"day": "Jour",
|
||||
"from": "De",
|
||||
"to": "À",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "Aucune date n’a été fixée pour cette tâche."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Membre de l’équipe ajouté.",
|
||||
"madeMember": "Le membre de l’équipe est devenu membre.",
|
||||
"madeAdmin": "Membre de l’équipe nommé admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Supprimer l’équipe",
|
||||
"text1": "Supprimer cette équipe et tous ses membres ?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"welcomeNight": "Buonanotte {username}!",
|
||||
"welcomeMorning": "Buongiorno {username}!",
|
||||
"welcomeDay": "Ciao {username}!",
|
||||
"welcomeEvening": "Buonasera {username}!",
|
||||
"lastViewed": "Ultima visualizzazione",
|
||||
"list": {
|
||||
"newText": "È possibile creare una nuova lista per le nuove attività:",
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Predefinito",
|
||||
"month": "Mese",
|
||||
"day": "Giorno",
|
||||
"from": "Da",
|
||||
"to": "A",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "Questa attività non ha date impostate."
|
||||
},
|
||||
"table": {
|
||||
|
@ -672,23 +672,23 @@
|
|||
"updated": "Aggiornato"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
|
||||
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
|
||||
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
|
||||
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
|
||||
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
|
||||
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
|
||||
"notSubscribedList": "You are not subscribed to this list 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.",
|
||||
"subscribedListThroughParentNamespace": "Non puoi annullare l'iscrizione perché sei iscritto al namespace di questa lista.",
|
||||
"subscribedTaskThroughParentNamespace": "Non puoi annullare l'iscrizione perché sei iscritto al namespace di questa attività.",
|
||||
"subscribedTaskThroughParentList": "Non puoi annullare l'iscrizione perché sei iscritto alla lista di questa attività.",
|
||||
"subscribedNamespace": "Sei iscritto a questo namespace e verrai notificato delle modifiche.",
|
||||
"notSubscribedNamespace": "Non sei iscritto a questo namespace e non verrai notificato delle modifiche.",
|
||||
"subscribedList": "Sei iscritto a questa lista e verrai notificato delle modifiche.",
|
||||
"notSubscribedList": "Non sei iscritto a questa lista e non verrai notificato delle modifiche.",
|
||||
"subscribedTask": "Sei iscritto a questa attività e verrai notificato delle modifiche.",
|
||||
"notSubscribedTask": "Non sei iscritto a questa attività e non verrai notificato delle modifiche.",
|
||||
"subscribe": "Iscriviti",
|
||||
"unsubscribe": "Disiscriviti",
|
||||
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
|
||||
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
|
||||
"subscribeSuccessList": "You are now subscribed to this list",
|
||||
"unsubscribeSuccessList": "You are now unsubscribed to this list",
|
||||
"subscribeSuccessTask": "You are now subscribed to this task",
|
||||
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
|
||||
"subscribeSuccessNamespace": "Sei iscritto a questo namespace",
|
||||
"unsubscribeSuccessNamespace": "Non sei più iscritto a questo namespace",
|
||||
"subscribeSuccessList": "Sei iscritto a questa lista",
|
||||
"unsubscribeSuccessList": "Non sei più iscritto a questa lista",
|
||||
"subscribeSuccessTask": "Sei iscritto a questa attività",
|
||||
"unsubscribeSuccessTask": "Non sei più iscritto a questa attività"
|
||||
},
|
||||
"attachment": {
|
||||
"title": "Allegati",
|
||||
|
@ -701,10 +701,10 @@
|
|||
"deleteText1": "Sei sicuro di voler eliminare l'allegato {filename}?",
|
||||
"copyUrl": "Copia URL",
|
||||
"copyUrlTooltip": "Copia l'URL di questo allegato per usarlo nel testo",
|
||||
"setAsCover": "Make cover",
|
||||
"unsetAsCover": "Remove cover",
|
||||
"successfullyChangedCoverImage": "The cover image was successfully changed.",
|
||||
"usedAsCover": "Cover image"
|
||||
"setAsCover": "Crea copertina",
|
||||
"unsetAsCover": "Rimuovi copertina",
|
||||
"successfullyChangedCoverImage": "L'immagine di copertina è stata cambiata con successo.",
|
||||
"usedAsCover": "Immagine di copertina"
|
||||
},
|
||||
"comment": {
|
||||
"title": "Commenti",
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Membro del gruppo aggiunto.",
|
||||
"madeMember": "Membro del gruppo reso membro.",
|
||||
"madeAdmin": "Membro del gruppo reso amministratore.",
|
||||
"mustSelectUser": "Seleziona un utente.",
|
||||
"delete": {
|
||||
"header": "Elimina il gruppo",
|
||||
"text1": "Sei sicuro di voler eliminare questo gruppo e tutti i suoi membri?",
|
||||
|
@ -855,10 +856,10 @@
|
|||
"success": "Utente rimosso dal gruppo."
|
||||
},
|
||||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
"title": "Abbandona il gruppo",
|
||||
"text1": "Sei sicuro di voler abbandonare questo gruppo?",
|
||||
"text2": "Perderai l'accesso a tutte le liste e namespace a cui questo gruppo ha accesso. Se cambi idea, dovrai farti aggiungere di nuovo da un amministratore del gruppo.",
|
||||
"success": "Hai abbandonato il gruppo."
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Rechten",
|
||||
"title": "Permission",
|
||||
"read": "Alleen lezen",
|
||||
"readWrite": "Lezen & schrijven",
|
||||
"admin": "Beheerder"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Standaard",
|
||||
"month": "Maand",
|
||||
"day": "Dag",
|
||||
"from": "Van",
|
||||
"to": "Aan",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Het teamlid is succesvol toegevoegd.",
|
||||
"madeMember": "Het teamlid is succesvol tot lid gemaakt.",
|
||||
"madeAdmin": "Het teamlid is succesvol beheerder gemaakt.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Verwijder het team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "{type} został pomyślnie dodany."
|
||||
},
|
||||
"right": {
|
||||
"title": "Uprawnienia",
|
||||
"title": "Permission",
|
||||
"read": "Tylko do odczytu",
|
||||
"readWrite": "Odczyt i zapis",
|
||||
"admin": "Administrator"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Domyślny",
|
||||
"month": "Miesiąc",
|
||||
"day": "Dzień",
|
||||
"from": "Od",
|
||||
"to": "Do",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "To zadanie nie ma ustawionych dat."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Członek zespołu został pomyślnie dodany.",
|
||||
"madeMember": "Użytkownik został pomyślnie mianowany członkiem zespołu.",
|
||||
"madeAdmin": "Członek zespołu został pomyślnie mianowany administratorem.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Usuń zespół",
|
||||
"text1": "Czy na pewno chcesz usunąć ten zespół i wszystkich jego członków?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "Good Night {username}!",
|
||||
"welcomeMorning": "Good Morning {username}!",
|
||||
"welcomeDay": "Hi {username}!",
|
||||
"welcomeEvening": "Good Evening {username}!",
|
||||
"lastViewed": "Last viewed",
|
||||
"welcomeNight": "Boa noite, {username}!",
|
||||
"welcomeMorning": "Bom dia, {username}!",
|
||||
"welcomeDay": "Olá, {username}!",
|
||||
"welcomeEvening": "Boa noite, {username}!",
|
||||
"lastViewed": "Visto por último",
|
||||
"list": {
|
||||
"newText": "You can create a new list for your new tasks:",
|
||||
"new": "New list",
|
||||
"newText": "Você pode criar uma nova lista para suas novas tarefas:",
|
||||
"new": "Nova lista",
|
||||
"importText": "Or import your lists and tasks from other services into Vikunja:",
|
||||
"import": "Import your data into Vikunja"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"title": "Not found",
|
||||
"title": "Não encontrado",
|
||||
"text": "The page you requested does not exist."
|
||||
},
|
||||
"ready": {
|
||||
"loading": "Vikunja is loading…",
|
||||
"errorOccured": "An error occurred:",
|
||||
"errorOccured": "Ocorreu um erro:",
|
||||
"checkApiUrl": "Please check if the api url is correct.",
|
||||
"noApiUrlConfigured": "No API url was configured. Please set one below:"
|
||||
"noApiUrlConfigured": "Nenhuma URL de API foi configurada. Por favor, defina uma abaixo:"
|
||||
},
|
||||
"offline": {
|
||||
"title": "You are offline.",
|
||||
|
@ -28,10 +28,10 @@
|
|||
},
|
||||
"user": {
|
||||
"auth": {
|
||||
"username": "Username",
|
||||
"usernameEmail": "Username Or Email Address",
|
||||
"username": "Nome de usuário",
|
||||
"usernameEmail": "Nome de usuário ou endereço de e-mail",
|
||||
"usernamePlaceholder": "e.g. frederick",
|
||||
"email": "Email address",
|
||||
"email": "Endereço de e-mail",
|
||||
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "e.g. •••••••••••",
|
||||
|
@ -44,20 +44,20 @@
|
|||
"totpTitle": "Two Factor Authentication Code",
|
||||
"totpPlaceholder": "e.g. 123456",
|
||||
"login": "Login",
|
||||
"createAccount": "Create account",
|
||||
"createAccount": "Criar conta",
|
||||
"loginWith": "Log in with {provider}",
|
||||
"authenticating": "Authenticating…",
|
||||
"openIdStateError": "State does not match, refusing to continue!",
|
||||
"openIdGeneralError": "An error occured while authenticating against the third party.",
|
||||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
"alreadyHaveAnAccount": "Already have an account?",
|
||||
"remember": "Stay logged in"
|
||||
"emailInvalid": "Por favor, insira um endereço de e-mail válido.",
|
||||
"usernameRequired": "Por favor, insira um nome de usuário.",
|
||||
"passwordRequired": "Por favor, insira uma senha.",
|
||||
"showPassword": "Exibir senha",
|
||||
"hidePassword": "Ocultar senha",
|
||||
"noAccountYet": "Não possui uma conta ainda?",
|
||||
"alreadyHaveAnAccount": "Já possui uma conta?",
|
||||
"remember": "Permanecer conectado"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
|
@ -68,7 +68,7 @@
|
|||
"currentPasswordPlaceholder": "Your current password",
|
||||
"passwordsDontMatch": "The new password and its confirmation don't match.",
|
||||
"passwordUpdateSuccess": "The password was successfully updated.",
|
||||
"updateEmailTitle": "Update Your Email Address",
|
||||
"updateEmailTitle": "Atualizar seu endereço de e-mail",
|
||||
"updateEmailNew": "New Email Address",
|
||||
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
|
||||
"general": {
|
||||
|
@ -86,34 +86,34 @@
|
|||
"weekStartMonday": "Monday",
|
||||
"language": "Language",
|
||||
"defaultList": "Default List",
|
||||
"timezone": "Time Zone",
|
||||
"timezone": "Fuso horário",
|
||||
"overdueTasksRemindersTime": "Overdue tasks reminder email time"
|
||||
},
|
||||
"totp": {
|
||||
"title": "Two Factor Authentication",
|
||||
"enroll": "Enroll",
|
||||
"enroll": "Inscrever-se",
|
||||
"finishSetupPart1": "To finish your setup, use this secret in your totp app (Google Authenticator or similar):",
|
||||
"finishSetupPart2": "After that, enter a code from your app below.",
|
||||
"scanQR": "Alternatively you can scan this QR code:",
|
||||
"passcode": "Passcode",
|
||||
"passcodePlaceholder": "A code generated by your totp application",
|
||||
"setupSuccess": "You've successfully set up two factor authentication!",
|
||||
"setupSuccess": "Você configurou a autenticação de dois fatores com sucesso!",
|
||||
"enterPassword": "Please Enter Your Password",
|
||||
"disable": "Disable two factor authentication",
|
||||
"confirmSuccess": "You've successfully confirmed your totp setup and can use it from now on!",
|
||||
"disableSuccess": "Two factor authentication was successfully disabled."
|
||||
"disableSuccess": "A autenticação de dois fatores foi desativada com êxito."
|
||||
},
|
||||
"caldav": {
|
||||
"title": "CalDAV",
|
||||
"howTo": "You can connect Vikunja to CalDAV clients to view and manage all tasks from different clients. Enter this url into your client:",
|
||||
"more": "More information about CalDAV in Vikunja",
|
||||
"howTo": "Você pode conectar o Vikunja aos clientes de CalDAV para visualizar e gerenciar todas as tarefas de diferentes clientes. Digite esta url em seu cliente:",
|
||||
"more": "Mais informações sobre CalDAV em Vikunja",
|
||||
"tokens": "CalDAV Tokens",
|
||||
"tokensHowTo": "You can use a CalDAV token to use instead of a password to log in the above endpoint.",
|
||||
"createToken": "Create a token",
|
||||
"tokenCreated": "Here is your token: {token}",
|
||||
"wontSeeItAgain": "Write it down, you won't be able to see it again.",
|
||||
"mustUseToken": "You need to create a CalDAV token if you want to use CalDAV with a third party client. Use the token as the password.",
|
||||
"usernameIs": "Your username is: {0}"
|
||||
"tokensHowTo": "Você pode usar um token CalDAV em vez de uma senha para fazer o login no endpoint acima.",
|
||||
"createToken": "Criar um token",
|
||||
"tokenCreated": "Aqui está seu token: {token}",
|
||||
"wontSeeItAgain": "Anote isso, você não poderá vê-lo novamente.",
|
||||
"mustUseToken": "Você precisa criar um token CalDAV se quiser usar CalDAV com um cliente de terceiros. Use o token como a senha.",
|
||||
"usernameIs": "Seu usuário é: {0}"
|
||||
},
|
||||
"avatar": {
|
||||
"title": "Avatar",
|
||||
|
@ -173,16 +173,16 @@
|
|||
"title": "List",
|
||||
"add": "Add",
|
||||
"addPlaceholder": "Add a new task…",
|
||||
"empty": "This list is currently empty.",
|
||||
"newTaskCta": "Create a new task.",
|
||||
"editTask": "Edit Task"
|
||||
"empty": "Esta lista está atualmente vazia.",
|
||||
"newTaskCta": "Criar uma nova tarefa.",
|
||||
"editTask": "Editar Tarefa"
|
||||
},
|
||||
"search": "Type to search for a list…",
|
||||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
"noDescriptionAvailable": "No list description is available.",
|
||||
"create": {
|
||||
"header": "New list",
|
||||
"header": "Nova lista",
|
||||
"titlePlaceholder": "The list's title goes here…",
|
||||
"addTitleRequired": "Please specify a title.",
|
||||
"createdSuccess": "The list was successfully created.",
|
||||
|
@ -212,8 +212,8 @@
|
|||
"text1": "Are you sure you want to delete this list and all of its contents?",
|
||||
"text2": "This includes all tasks and CANNOT BE UNDONE!",
|
||||
"success": "The list was successfully deleted.",
|
||||
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
|
||||
"noTasksToDelete": "This list does not contain any tasks, it should be safe to delete."
|
||||
"tasksToDelete": "Isto irá remover irrevogavelmente aprox. {count} tarefas.",
|
||||
"noTasksToDelete": "Esta lista não contém tarefas, deve ser segura para excluir."
|
||||
},
|
||||
"duplicate": {
|
||||
"title": "Duplicate this list",
|
||||
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permissão",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -280,261 +280,261 @@
|
|||
},
|
||||
"gantt": {
|
||||
"title": "Gantt",
|
||||
"showTasksWithoutDates": "Show tasks which don't have dates set",
|
||||
"size": "Size",
|
||||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"noDates": "This task has no dates set."
|
||||
"showTasksWithoutDates": "Mostrar tarefas que não possuem datas definidas",
|
||||
"size": "Tamanho",
|
||||
"default": "Padrão",
|
||||
"month": "Mês",
|
||||
"day": "Dia",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "Esta tarefa não tem datas definidas."
|
||||
},
|
||||
"table": {
|
||||
"title": "Table",
|
||||
"columns": "Columns"
|
||||
"title": "Tabela",
|
||||
"columns": "Colunas"
|
||||
},
|
||||
"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"
|
||||
"limit": "Limite: {limit}",
|
||||
"noLimit": "Não definido",
|
||||
"doneBucket": "Bucket concluído",
|
||||
"doneBucketHint": "Todas as tarefas movidas para este bucket serão marcadas automaticamente como concluídas.",
|
||||
"doneBucketHintExtended": "Todas as tarefas movidas para o bucket concluído serão automaticamente concluídas também. Todas as tarefas concluídas de outro lugar serão movidas também.",
|
||||
"doneBucketSavedSuccess": "O bucket foi marcado como concluído com sucesso.",
|
||||
"deleteLast": "Você não pode remover o último bucket.",
|
||||
"addTaskPlaceholder": "Digite o novo título da tarefa…",
|
||||
"addTask": "Adicionar uma tarefa",
|
||||
"addAnotherTask": "Adicionar outra tarefa",
|
||||
"addBucket": "Criar um novo bucket",
|
||||
"addBucketPlaceholder": "Digite o novo título do bucket…",
|
||||
"deleteHeaderBucket": "Excluir o bucket",
|
||||
"deleteBucketText1": "Tem certeza que deseja excluir este bucket?",
|
||||
"deleteBucketText2": "Isto não vai apagar nenhuma tarefa, mas as moverá para o bucket padrão.",
|
||||
"deleteBucketSuccess": "O bucket foi excluído com sucesso.",
|
||||
"bucketTitleSavedSuccess": "O título do bucket foi salvo com sucesso.",
|
||||
"bucketLimitSavedSuccess": "O limite do bucket foi salvo com sucesso.",
|
||||
"collapse": "Recolher este bucket"
|
||||
},
|
||||
"pseudo": {
|
||||
"favorites": {
|
||||
"title": "Favorites"
|
||||
"title": "Favoritos"
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"title": "Namespaces & Lists",
|
||||
"title": "Listas & Namespaces",
|
||||
"namespace": "Namespace",
|
||||
"showArchived": "Show Archived",
|
||||
"noneAvailable": "You don't have any namespaces right now.",
|
||||
"unarchive": "Un-Archive",
|
||||
"archived": "Archived",
|
||||
"noLists": "This namespace does not contain any lists.",
|
||||
"createList": "Create a new list in this namespace.",
|
||||
"noneAvailable": "Você não tem nenhum namespace no momento.",
|
||||
"unarchive": "Desarquivar",
|
||||
"archived": "Arquivado",
|
||||
"noLists": "Este namespace não contém nenhuma lista.",
|
||||
"createList": "Criar uma nova lista neste namespace.",
|
||||
"namespaces": "Namespaces",
|
||||
"search": "Type to search for a namespace…",
|
||||
"search": "Digite para procurar por um namespace…",
|
||||
"create": {
|
||||
"title": "New namespace",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namespace.",
|
||||
"tooltip": "What's a namespace?",
|
||||
"success": "The namespace was successfully created."
|
||||
"title": "Novo namespace",
|
||||
"titleRequired": "Por favor, especifique um título.",
|
||||
"explanation": "Um namespace é uma coleção de listas que você pode compartilhar e usar para organizar suas listas. Na verdade, todas as listas pertencem a um namespace.",
|
||||
"tooltip": "O que é um namespace?",
|
||||
"success": "O namespace foi criado com sucesso."
|
||||
},
|
||||
"archive": {
|
||||
"titleArchive": "Archive \"{namespace}\"",
|
||||
"titleUnarchive": "Un-Archive \"{namespace}\"",
|
||||
"titleArchive": "Arquivar \"{namespace}\"",
|
||||
"titleUnarchive": "Desarquivar \"{namespace}\"",
|
||||
"archiveText": "You won't be able to edit this namespace or create new lists until you un-archive it. This will also archive all lists in this namespace.",
|
||||
"unarchiveText": "You will be able to create new lists or edit it.",
|
||||
"success": "The namespace was successfully archived.",
|
||||
"unarchiveSuccess": "The namespace was successfully un-archived.",
|
||||
"unarchiveText": "Você será capaz de criar novas listas ou editá-las.",
|
||||
"success": "O namespace foi arquivado com sucesso.",
|
||||
"unarchiveSuccess": "O namespace foi desarquivado com sucesso.",
|
||||
"description": "If a namespace is archived, you cannot create new lists or edit it."
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete \"{namespace}\"",
|
||||
"title": "Excluir \"{namespace}\"",
|
||||
"text1": "Are you sure you want to delete this namespace and all of its contents?",
|
||||
"text2": "This includes all lists and tasks and CANNOT BE UNDONE!",
|
||||
"success": "The namespace was successfully deleted."
|
||||
"success": "O namespace foi excluído com sucesso."
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit \"{namespace}\"",
|
||||
"success": "The namespace was successfully updated."
|
||||
"title": "Editar \"{namespace}\"",
|
||||
"success": "O namespace foi atualizado com sucesso."
|
||||
},
|
||||
"share": {
|
||||
"title": "Share \"{namespace}\""
|
||||
"title": "Compartilhar \"{namespace}\""
|
||||
},
|
||||
"attributes": {
|
||||
"title": "Namespace Title",
|
||||
"titlePlaceholder": "The namespace title goes here…",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "The namespaces description goes here…",
|
||||
"color": "Color",
|
||||
"archived": "Is Archived",
|
||||
"isArchived": "This namespace is archived"
|
||||
"title": "Título do Namespace",
|
||||
"titlePlaceholder": "O título do namespace fica aqui…",
|
||||
"description": "Descrição",
|
||||
"descriptionPlaceholder": "A descrição do namespace fica aqui…",
|
||||
"color": "Cor",
|
||||
"archived": "Está arquivado",
|
||||
"isArchived": "Este namespace está arquivado"
|
||||
},
|
||||
"pseudo": {
|
||||
"sharedLists": {
|
||||
"title": "Shared Lists"
|
||||
"title": "Listas Compartilhadas"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favorites"
|
||||
"title": "Favoritos"
|
||||
},
|
||||
"savedFilters": {
|
||||
"title": "Filters"
|
||||
"title": "Filtros"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"title": "Filters",
|
||||
"clear": "Clear Filters",
|
||||
"title": "Filtros",
|
||||
"clear": "Limpar Filtros",
|
||||
"attributes": {
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "The saved filter title goes here…",
|
||||
"description": "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",
|
||||
"showDoneTasks": "Show Done Tasks",
|
||||
"sortAlphabetically": "Sort Alphabetically",
|
||||
"enablePriority": "Enable Filter By Priority",
|
||||
"enablePercentDone": "Enable Filter By Progress",
|
||||
"title": "Título",
|
||||
"titlePlaceholder": "O título do filtro salvo fica aqui…",
|
||||
"description": "Descrição",
|
||||
"descriptionPlaceholder": "A descrição fica aqui…",
|
||||
"includeNulls": "Incluir tarefas que não possuem um conjunto de valores",
|
||||
"requireAll": "Exigir que todos os filtros sejam verdadeiros para uma tarefa ser exibida",
|
||||
"showDoneTasks": "Mostrar tarefas concluídas",
|
||||
"sortAlphabetically": "Ordernar alfabeticamente",
|
||||
"enablePriority": "Ativar Filtro por Prioridade",
|
||||
"enablePercentDone": "Ativar Filtro por Progresso",
|
||||
"dueDateRange": "Due Date Range",
|
||||
"startDateRange": "Start Date Range",
|
||||
"startDateRange": "Data de início",
|
||||
"endDateRange": "End Date Range",
|
||||
"reminderRange": "Reminder Date Range"
|
||||
},
|
||||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"title": "Novo filtro salvo",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
"text": "Are you sure you want to delete this saved filter?",
|
||||
"success": "The filter was deleted successfully."
|
||||
"success": "O filtro foi excluído com sucesso."
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit This Saved Filter",
|
||||
"success": "The filter was saved successfully."
|
||||
"success": "O filtro foi salvo com sucesso."
|
||||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Import your data into Vikunja",
|
||||
"import": "Importe seus dados para o Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
"descriptionDo": "Vikunja will import all lists, tasks, notes, reminders and files you have access to.",
|
||||
"authorize": "To authorize Vikunja to access your {name} Account, click the button below.",
|
||||
"getStarted": "Get Started",
|
||||
"inProgress": "Importing in progress…",
|
||||
"inProgress": "Importação em andamento…",
|
||||
"alreadyMigrated1": "It looks like you've already imported your stuff from {name} at {date}.",
|
||||
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
|
||||
"confirm": "I am sure, please start migrating now!",
|
||||
"confirm": "Tenho certeza, comece a migrar agora, por favor!",
|
||||
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
|
||||
"upload": "Upload file"
|
||||
"upload": "Enviar arquivo"
|
||||
},
|
||||
"label": {
|
||||
"title": "Labels",
|
||||
"manage": "Manage labels",
|
||||
"title": "Etiquetas",
|
||||
"manage": "Editar etiquetas",
|
||||
"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 list you have access.",
|
||||
"newCTA": "You currently do not have any labels.",
|
||||
"search": "Type to search for a label…",
|
||||
"newCTA": "Você não tem nenhuma etiqueta atualmente.",
|
||||
"search": "Digite para procurar por uma etiqueta…",
|
||||
"create": {
|
||||
"header": "New label",
|
||||
"title": "Create a new label",
|
||||
"titleRequired": "Please specify a title.",
|
||||
"success": "The label was successfully created."
|
||||
"header": "Nova etiqueta",
|
||||
"title": "Criar uma nova etiqueta",
|
||||
"titleRequired": "Por favor, especifique um título.",
|
||||
"success": "A etiqueta foi criada com sucesso."
|
||||
},
|
||||
"edit": {
|
||||
"header": "Edit Label",
|
||||
"header": "Editar etiqueta",
|
||||
"forbidden": "You are not allowed to edit this label because you dont own it.",
|
||||
"success": "The label was successfully updated."
|
||||
"success": "A etiqueta foi atualizada com sucesso."
|
||||
},
|
||||
"deleteSuccess": "The label was successfully deleted.",
|
||||
"deleteSuccess": "A etiqueta foi excluída com sucesso.",
|
||||
"attributes": {
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "The label title goes here…",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Label description",
|
||||
"color": "Color"
|
||||
"title": "Título",
|
||||
"titlePlaceholder": "O título da etiqueta fica aqui…",
|
||||
"description": "Descrição",
|
||||
"descriptionPlaceholder": "Descrição da etiqueta",
|
||||
"color": "Cor"
|
||||
}
|
||||
},
|
||||
"sharing": {
|
||||
"authenticating": "Authenticating…",
|
||||
"passwordRequired": "This shared list requires a password. Please enter it below:",
|
||||
"error": "An error occured.",
|
||||
"invalidPassword": "The password is invalid."
|
||||
"authenticating": "Autenticando…",
|
||||
"passwordRequired": "Esta lista compartilhada requer uma senha. Por favor, digite-a abaixo:",
|
||||
"error": "Ocorreu um erro.",
|
||||
"invalidPassword": "A senha é inválida."
|
||||
},
|
||||
"navigation": {
|
||||
"overview": "Overview",
|
||||
"overview": "Visão geral",
|
||||
"upcoming": "Upcoming",
|
||||
"settings": "Settings",
|
||||
"imprint": "Imprint",
|
||||
"privacy": "Privacy Policy"
|
||||
"settings": "Configurações",
|
||||
"imprint": "Imprimir",
|
||||
"privacy": "Política de Privacidade"
|
||||
},
|
||||
"misc": {
|
||||
"loading": "Loading…",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"refresh": "Refresh",
|
||||
"disable": "Disable",
|
||||
"copy": "Copy to clipboard",
|
||||
"copyError": "Copy to clipboard failed",
|
||||
"search": "Search",
|
||||
"searchPlaceholder": "Type to search…",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"poweredBy": "Powered by Vikunja",
|
||||
"loading": "Carregando…",
|
||||
"save": "Salvar",
|
||||
"delete": "Excluir",
|
||||
"confirm": "Confirmar",
|
||||
"cancel": "Cancelar",
|
||||
"refresh": "Atualizar",
|
||||
"disable": "Desativar",
|
||||
"copy": "Copiar para área de transferência",
|
||||
"copyError": "Falha ao copiar para área de transferência",
|
||||
"search": "Pesquisar",
|
||||
"searchPlaceholder": "Digite para pesquisar…",
|
||||
"previous": "Voltar",
|
||||
"next": "Avançar",
|
||||
"poweredBy": "Desenvolvido por Vikunja",
|
||||
"info": "Info",
|
||||
"create": "Create",
|
||||
"create": "Criar",
|
||||
"doit": "Do it!",
|
||||
"saving": "Saving…",
|
||||
"saved": "Saved!",
|
||||
"default": "Default",
|
||||
"close": "Close",
|
||||
"download": "Download",
|
||||
"showMenu": "Show the menu",
|
||||
"hideMenu": "Hide the menu",
|
||||
"forExample": "For example:",
|
||||
"welcomeBack": "Welcome Back!",
|
||||
"saving": "Salvando…",
|
||||
"saved": "Salvo!",
|
||||
"default": "Padrão",
|
||||
"close": "Fechar",
|
||||
"download": "Baixar",
|
||||
"showMenu": "Mostrar o menu",
|
||||
"hideMenu": "Esconder o menu",
|
||||
"forExample": "Por exemplo:",
|
||||
"welcomeBack": "Bem-vindo de volta!",
|
||||
"custom": "Custom",
|
||||
"id": "ID",
|
||||
"created": "Created at",
|
||||
"actions": "Actions",
|
||||
"created": "Criado em",
|
||||
"actions": "Ações",
|
||||
"cannotBeUndone": "This cannot be undone!"
|
||||
},
|
||||
"input": {
|
||||
"resetColor": "Reset Color",
|
||||
"resetColor": "Restaurar Cor",
|
||||
"datepicker": {
|
||||
"today": "Today",
|
||||
"tomorrow": "Tomorrow",
|
||||
"nextMonday": "Next Monday",
|
||||
"thisWeekend": "This Weekend",
|
||||
"today": "Hoje",
|
||||
"tomorrow": "Amanhã",
|
||||
"nextMonday": "Próxima segunda-feira",
|
||||
"thisWeekend": "Este fim de semana",
|
||||
"laterThisWeek": "Later This Week",
|
||||
"nextWeek": "Next Week",
|
||||
"chooseDate": "Choose a date"
|
||||
"nextWeek": "Próxima semana",
|
||||
"chooseDate": "Escolha uma data"
|
||||
},
|
||||
"editor": {
|
||||
"edit": "Edit",
|
||||
"done": "Done",
|
||||
"heading1": "Heading 1",
|
||||
"heading2": "Heading 2",
|
||||
"heading3": "Heading 3",
|
||||
"edit": "Editar",
|
||||
"done": "Concluído",
|
||||
"heading1": "Título 1",
|
||||
"heading2": "Título 2",
|
||||
"heading3": "Título 3",
|
||||
"headingSmaller": "Heading Smaller",
|
||||
"headingBigger": "Heading Bigger",
|
||||
"bold": "Bold",
|
||||
"italic": "Italic",
|
||||
"strikethrough": "Strikethrough",
|
||||
"code": "Code",
|
||||
"quote": "Quote",
|
||||
"unorderedList": "Unordered List",
|
||||
"orderedList": "Ordered List",
|
||||
"bold": "Negrito",
|
||||
"italic": "Itálico",
|
||||
"strikethrough": "Riscado",
|
||||
"code": "Código",
|
||||
"quote": "Citação",
|
||||
"unorderedList": "Lista não ordenada",
|
||||
"orderedList": "Lista ordenada",
|
||||
"cleanBlock": "Clean Block",
|
||||
"link": "Link",
|
||||
"image": "Image",
|
||||
"table": "Table",
|
||||
"horizontalRule": "Horizontal Rule",
|
||||
"sideBySide": "Side By Side",
|
||||
"image": "Imagem",
|
||||
"table": "Tabela",
|
||||
"horizontalRule": "Linha horizontal",
|
||||
"sideBySide": "Lado a Lado",
|
||||
"guide": "Guide"
|
||||
},
|
||||
"multiselect": {
|
||||
|
@ -589,80 +589,80 @@
|
|||
"beginningOfThisWeek": "The beginning of this week at 00:00",
|
||||
"endOfThisWeek": "The end of this week",
|
||||
"in30Days": "In 30 days",
|
||||
"datePlusMonth": "{0} plus one month at 00:00 of that day"
|
||||
"datePlusMonth": "{0} mais um mês às 00:00 desse dia"
|
||||
}
|
||||
}
|
||||
},
|
||||
"task": {
|
||||
"task": "Task",
|
||||
"new": "Create a new task",
|
||||
"delete": "Delete this task",
|
||||
"createSuccess": "The task was successfully created.",
|
||||
"addReminder": "Add a new reminder…",
|
||||
"doneSuccess": "The task was successfully marked as done.",
|
||||
"undoneSuccess": "The task was successfully un-marked as done.",
|
||||
"openDetail": "Open task detail view",
|
||||
"checklistTotal": "{checked} of {total} tasks",
|
||||
"checklistAllDone": "{total} tasks",
|
||||
"task": "Tarefa",
|
||||
"new": "Criar uma nova tarefa",
|
||||
"delete": "Excluir esta tarefa",
|
||||
"createSuccess": "A tarefa foi criada com sucesso.",
|
||||
"addReminder": "Adicionar um novo lembrete…",
|
||||
"doneSuccess": "A tarefa foi marcada como feita com sucesso.",
|
||||
"undoneSuccess": "A tarefa foi desmarcada como feita com sucesso.",
|
||||
"openDetail": "Abrir detalhes da tarefa",
|
||||
"checklistTotal": "{checked} de {total} tarefas",
|
||||
"checklistAllDone": "{total} tarefas",
|
||||
"show": {
|
||||
"titleCurrent": "Current Tasks",
|
||||
"titleDates": "Tasks from {from} until {to}",
|
||||
"noDates": "Show tasks without dates",
|
||||
"overdue": "Show overdue tasks",
|
||||
"titleCurrent": "Tarefas atuais",
|
||||
"titleDates": "Tarefas de {from} até {to}",
|
||||
"noDates": "Mostrar tarefas sem datas",
|
||||
"overdue": "Mostrar tarefas atrasadas",
|
||||
"fromuntil": "Tasks from {from} until {until}",
|
||||
"select": "Select a date range",
|
||||
"noTasks": "Nothing to do — Have a nice day!"
|
||||
"select": "Selecione um intervalo de datas",
|
||||
"noTasks": "Nada a fazer — Tenha um ótimo dia!"
|
||||
},
|
||||
"detail": {
|
||||
"chooseDueDate": "Click here to set a due date",
|
||||
"chooseStartDate": "Click here to set a start date",
|
||||
"chooseEndDate": "Click here to set an end date",
|
||||
"move": "Move task to a different list",
|
||||
"done": "Mark task done!",
|
||||
"undone": "Mark as undone",
|
||||
"created": "Created {0} by {1}",
|
||||
"updated": "Updated {0}",
|
||||
"doneAt": "Done {0}",
|
||||
"updateSuccess": "The task was saved successfully.",
|
||||
"deleteSuccess": "The task has been deleted successfully.",
|
||||
"done": "Marcar tarefa como concluída!",
|
||||
"undone": "Marcar como não concluído",
|
||||
"created": "Criado {0} por {1}",
|
||||
"updated": "Atualizado {0}",
|
||||
"doneAt": "Concluído {0}",
|
||||
"updateSuccess": "A tarefa foi salva com sucesso.",
|
||||
"deleteSuccess": "A tarefa foi excluída com sucesso.",
|
||||
"belongsToList": "This task belongs to list '{list}'",
|
||||
"due": "Due {at}",
|
||||
"closePopup": "Close popup",
|
||||
"closePopup": "Fechar janela",
|
||||
"delete": {
|
||||
"header": "Delete this task",
|
||||
"text1": "Are you sure you want to remove this task?",
|
||||
"header": "Excluir esta tarefa",
|
||||
"text1": "Tem certeza que deseja remover esta tarefa?",
|
||||
"text2": "This will also remove all attachments, reminders and relations associated with this task and cannot be undone!"
|
||||
},
|
||||
"actions": {
|
||||
"assign": "Assign to User",
|
||||
"label": "Add Labels",
|
||||
"priority": "Set Priority",
|
||||
"dueDate": "Set Due Date",
|
||||
"startDate": "Set Start Date",
|
||||
"endDate": "Set End Date",
|
||||
"reminders": "Set Reminders",
|
||||
"assign": "Atribuir usuário",
|
||||
"label": "Adicionar etiquetas",
|
||||
"priority": "Definir prioridade",
|
||||
"dueDate": "Definir prazo",
|
||||
"startDate": "Definir data de início",
|
||||
"endDate": "Definir data de término",
|
||||
"reminders": "Definir lembretes",
|
||||
"repeatAfter": "Set Repeating Interval",
|
||||
"percentDone": "Set Progress",
|
||||
"attachments": "Add Attachments",
|
||||
"percentDone": "Definir progresso",
|
||||
"attachments": "Adicionar anexos",
|
||||
"relatedTasks": "Add Relation",
|
||||
"moveList": "Move",
|
||||
"color": "Set Color",
|
||||
"delete": "Delete",
|
||||
"favorite": "Add to Favorites",
|
||||
"unfavorite": "Remove from Favorites"
|
||||
"moveList": "Mover",
|
||||
"color": "Definir cor",
|
||||
"delete": "Excluir",
|
||||
"favorite": "Adicionar aos favoritos",
|
||||
"unfavorite": "Remover dos favoritos"
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"assignees": "Assignees",
|
||||
"color": "Color",
|
||||
"created": "Created",
|
||||
"createdBy": "Created By",
|
||||
"description": "Description",
|
||||
"done": "Done",
|
||||
"dueDate": "Due Date",
|
||||
"endDate": "End Date",
|
||||
"labels": "Labels",
|
||||
"percentDone": "Progress",
|
||||
"color": "Cor",
|
||||
"created": "Criado",
|
||||
"createdBy": "Criador por",
|
||||
"description": "Descrição",
|
||||
"done": "Concluído",
|
||||
"dueDate": "Data de vencimento",
|
||||
"endDate": "Data de término",
|
||||
"labels": "Etiquetas",
|
||||
"percentDone": "Progresso",
|
||||
"priority": "Priority",
|
||||
"relatedTasks": "Related Tasks",
|
||||
"reminders": "Reminders",
|
||||
|
@ -711,12 +711,12 @@
|
|||
"loading": "Loading comments…",
|
||||
"edited": "edited {date}",
|
||||
"creating": "Creating comment…",
|
||||
"placeholder": "Add your comment…",
|
||||
"comment": "Comment",
|
||||
"delete": "Delete this comment",
|
||||
"deleteText1": "Are you sure you want to delete this comment?",
|
||||
"deleteSuccess": "The comment was deleted successfully.",
|
||||
"addedSuccess": "The comment was added successfully."
|
||||
"placeholder": "Adicione seu comentário…",
|
||||
"comment": "Comentário",
|
||||
"delete": "Apagar este comentário",
|
||||
"deleteText1": "Tem certeza que deseja apagar este comentário?",
|
||||
"deleteSuccess": "O comentário foi apagado com sucesso.",
|
||||
"addedSuccess": "O comentário foi adicionado com sucesso."
|
||||
},
|
||||
"deferDueDate": {
|
||||
"title": "Defer due date",
|
||||
|
@ -825,55 +825,56 @@
|
|||
}
|
||||
},
|
||||
"team": {
|
||||
"title": "Teams",
|
||||
"title": "Equipes",
|
||||
"noTeams": "You are currently not part of any teams.",
|
||||
"create": {
|
||||
"title": "Create a new team",
|
||||
"success": "The team was successfully created."
|
||||
"title": "Criar uma equipe",
|
||||
"success": "A equipe foi criada com sucesso."
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit Team \"{team}\"",
|
||||
"members": "Team Members",
|
||||
"search": "Type to search a user…",
|
||||
"addUser": "Add to team",
|
||||
"addUser": "Adicionar à equipe",
|
||||
"makeMember": "Make Member",
|
||||
"makeAdmin": "Make Admin",
|
||||
"success": "The team was successfully updated.",
|
||||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"makeAdmin": "Tornar administrador",
|
||||
"success": "A equipe foi atualizada com sucesso.",
|
||||
"userAddedSuccess": "O membro da equipe foi adicionado com sucesso.",
|
||||
"madeMember": "O integrante da equipe foi promovido a membro com sucesso.",
|
||||
"madeAdmin": "O membro da equipe foi promovido a administrador com sucesso.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"header": "Excluir a equipe",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
"text2": "All team members will lose access to lists and namespaces shared with this team. This CANNOT BE UNDONE!",
|
||||
"success": "The team was successfully deleted."
|
||||
"success": "A equipe foi excluída com sucesso."
|
||||
},
|
||||
"deleteUser": {
|
||||
"header": "Remove a user from the team",
|
||||
"text1": "Are you sure you want to remove this user from the team?",
|
||||
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
|
||||
"success": "The user was successfully deleted from the team."
|
||||
"success": "O usuário foi removido da equipe com sucesso."
|
||||
},
|
||||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"name": "Team Name",
|
||||
"namePlaceholder": "The team's name goes here…",
|
||||
"nameRequired": "Please specify a name.",
|
||||
"nameRequired": "Por favor, especifique um nome.",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "The teams description goes here…",
|
||||
"admin": "Admin",
|
||||
"member": "Member"
|
||||
"admin": "Administrador",
|
||||
"member": "Membro"
|
||||
}
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"general": "General",
|
||||
"title": "Atalhos de teclado",
|
||||
"general": "Geral",
|
||||
"allPages": "These shortcuts work on all pages.",
|
||||
"currentPageOnly": "These shortcuts work only on the current page.",
|
||||
"somePagesOnly": "These shortcuts work only on some pages.",
|
||||
|
@ -885,7 +886,7 @@
|
|||
"done": "Mark task done / undone",
|
||||
"assign": "Assign this task to a user",
|
||||
"labels": "Add labels to this task",
|
||||
"dueDate": "Change the due date of this task",
|
||||
"dueDate": "Alterar a data de vencimento desta tarefa",
|
||||
"attachment": "Add an attachment to this task",
|
||||
"related": "Modify related tasks of this task",
|
||||
"color": "Change the color of this task",
|
||||
|
@ -914,10 +915,10 @@
|
|||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
"edit": "Edit",
|
||||
"archive": "Archive",
|
||||
"duplicate": "Duplicate",
|
||||
"delete": "Delete",
|
||||
"edit": "Editar",
|
||||
"archive": "Arquivar",
|
||||
"duplicate": "Duplicar",
|
||||
"delete": "Excluir",
|
||||
"unarchive": "Un-Archive",
|
||||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
|
@ -929,60 +930,60 @@
|
|||
"change": "change",
|
||||
"use": "Using Vikunja installation at {0}",
|
||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
||||
"success": "Using Vikunja installation at \"{domain}\".",
|
||||
"urlRequired": "A url is required."
|
||||
"success": "Usando a instalação Vikunja em \"{domain}\".",
|
||||
"urlRequired": "Uma url é necessária."
|
||||
},
|
||||
"loadingError": {
|
||||
"failed": "Loading failed, please {0}. If the error persists, please {1}.",
|
||||
"tryAgain": "try again",
|
||||
"contact": "contact us"
|
||||
"tryAgain": "tente novamente",
|
||||
"contact": "contate-nos"
|
||||
},
|
||||
"notification": {
|
||||
"title": "Notifications",
|
||||
"title": "Notificações",
|
||||
"none": "You don't have any notifications. Have a nice day!",
|
||||
"explainer": "Notifications will appear here when actions on namespaces, lists or tasks you subscribed to happen."
|
||||
},
|
||||
"quickActions": {
|
||||
"commands": "Commands",
|
||||
"placeholder": "Type a command or search…",
|
||||
"commands": "Comandos",
|
||||
"placeholder": "Digite um comando ou pesquise…",
|
||||
"hint": "You can use {list} to limit the search to a list. Combine {list} or {label} (labels) with a search query to search for a task with these labels or on that list. Use {assignee} to only search for teams.",
|
||||
"tasks": "Tasks",
|
||||
"lists": "Lists",
|
||||
"teams": "Teams",
|
||||
"newList": "Enter the title of the new list…",
|
||||
"tasks": "Tarefas",
|
||||
"lists": "Listas",
|
||||
"teams": "Equipes",
|
||||
"newList": "Digite o título da nova lista…",
|
||||
"newTask": "Enter the title of the new task…",
|
||||
"newNamespace": "Enter the title of the new namespace…",
|
||||
"newTeam": "Enter the name of the new team…",
|
||||
"createTask": "Create a task in the current list ({title})",
|
||||
"createList": "Create a list in the current namespace ({title})",
|
||||
"cmds": {
|
||||
"newTask": "New task",
|
||||
"newList": "New list",
|
||||
"newNamespace": "New namespace",
|
||||
"newTeam": "New team"
|
||||
"newTask": "Nova tarefa",
|
||||
"newList": "Nova lista",
|
||||
"newNamespace": "Novo namespace",
|
||||
"newTeam": "Nova equipe"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"locale": "en",
|
||||
"locale": "pt-br",
|
||||
"altFormatLong": "j M Y H:i",
|
||||
"altFormatShort": "j M Y"
|
||||
},
|
||||
"error": {
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"error": "Erro",
|
||||
"success": "Sucesso",
|
||||
"0001": "You're not allowed to do that.",
|
||||
"1001": "A user with this username already exists.",
|
||||
"1002": "A user with this email address already exists.",
|
||||
"1004": "No username and password specified.",
|
||||
"1005": "The user does not exist.",
|
||||
"1006": "Could not get the user id.",
|
||||
"1005": "O usuário não existe.",
|
||||
"1006": "Não foi possível obter o ID do usuário.",
|
||||
"1008": "No password reset token provided.",
|
||||
"1009": "Invalid password reset token.",
|
||||
"1010": "Invalid email confirm token.",
|
||||
"1011": "Wrong username or password.",
|
||||
"1012": "Email address of the user not confirmed.",
|
||||
"1013": "New password is empty.",
|
||||
"1014": "Old password is empty.",
|
||||
"1011": "Usuário ou senha incorretos.",
|
||||
"1012": "Endereço de e-mail do usuário não confirmado.",
|
||||
"1013": "A senha nova está vazia.",
|
||||
"1014": "A senha antiga está vazia.",
|
||||
"1015": "Totp is already enabled for this user.",
|
||||
"1016": "Totp is not enabled for this user.",
|
||||
"1017": "The totp passcode is invalid.",
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "{type} foi adicionado com sucesso."
|
||||
},
|
||||
"right": {
|
||||
"title": "Permissões",
|
||||
"title": "Permissão",
|
||||
"read": "Apenas de leitura",
|
||||
"readWrite": "Leitura e escrita",
|
||||
"admin": "Administração"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Padrão",
|
||||
"month": "Mês",
|
||||
"day": "Dia",
|
||||
"from": "De",
|
||||
"to": "Até",
|
||||
"hour": "Hora",
|
||||
"range": "Intervalo de Datas",
|
||||
"noDates": "Esta tarefa não tem datas definidas."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "O membro da equipa foi adicionado com sucesso.",
|
||||
"madeMember": "O membro da equipa foi tornado membro com sucesso.",
|
||||
"madeAdmin": "O membro da equipa foi tornado admin com sucesso.",
|
||||
"mustSelectUser": "Por favor, seleciona um utilizador.",
|
||||
"delete": {
|
||||
"header": "Eliminar equipa",
|
||||
"text1": "Tens a certeza que pretendes eliminar esta equipa e todos os seus membros?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Sair da equipa",
|
||||
"text1": "Tens a certeza de que queres sair desta equipa?",
|
||||
"text2": "Vais perder acesso a todas as listas e espaços a que esta equipa tem acesso. Se mudares de ideias, vais necessitar que um administrador da equipa te adicione novamente.",
|
||||
"text2": "Vais perder o acesso a todas as listas e espaços a que esta equipa tem acesso. Se mudares de ideias, vais necessitar que um administrador da equipa te adicione novamente.",
|
||||
"success": "Saíste da equipa com sucesso."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "{type} успешно добавлен."
|
||||
},
|
||||
"right": {
|
||||
"title": "Права",
|
||||
"title": "Permission",
|
||||
"read": "Только чтение",
|
||||
"readWrite": "Чтение и запись",
|
||||
"admin": "Админ"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "По умолчанию",
|
||||
"month": "Месяц",
|
||||
"day": "День",
|
||||
"from": "С",
|
||||
"to": "По",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "В этой задаче нет установленной даты."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Участник добавлен.",
|
||||
"madeMember": "Участник команды теперь участник.",
|
||||
"madeAdmin": "Участник команды теперь администратор.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Удалить команду",
|
||||
"text1": "Удалить эту команду вместе с участниками?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "{type} đã được thêm thành công."
|
||||
},
|
||||
"right": {
|
||||
"title": "Quyền hạn",
|
||||
"title": "Permission",
|
||||
"read": "Chỉ đọc",
|
||||
"readWrite": "Đọc & ghi",
|
||||
"admin": "Quản trị viên"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Mặc định",
|
||||
"month": "Tháng",
|
||||
"day": "Ngày",
|
||||
"from": "Từ",
|
||||
"to": "Đến",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "Công việc này không thiết lập ngày."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "Thành viên Team đã được thêm.",
|
||||
"madeMember": "Vai trò thanh viên đã được cập nhật.",
|
||||
"madeAdmin": "Vai trò thành viên đã được cập nhật.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Giải tán Team",
|
||||
"text1": "Bạn có chắc giải tán Team này không?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
"updatedSuccess": "The {type} was successfully added."
|
||||
},
|
||||
"right": {
|
||||
"title": "Right",
|
||||
"title": "Permission",
|
||||
"read": "Read only",
|
||||
"readWrite": "Read & write",
|
||||
"admin": "Admin"
|
||||
|
@ -285,8 +285,8 @@
|
|||
"default": "Default",
|
||||
"month": "Month",
|
||||
"day": "Day",
|
||||
"from": "From",
|
||||
"to": "To",
|
||||
"hour": "Hour",
|
||||
"range": "Date Range",
|
||||
"noDates": "This task has no dates set."
|
||||
},
|
||||
"table": {
|
||||
|
@ -842,6 +842,7 @@
|
|||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
|
@ -857,7 +858,7 @@
|
|||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import {computed, ref, watch} from 'vue'
|
||||
import type dayjs from 'dayjs'
|
||||
import type ILocale from 'dayjs/locale/*'
|
||||
|
||||
import {i18n, type SupportedLocale, type ISOLanguage} from '@/i18n'
|
||||
|
||||
export const DAYJS_LOCALE_MAPPING = {
|
||||
'de-de': 'de',
|
||||
'de-swiss': 'de-at',
|
||||
'ru-ru': 'ru',
|
||||
'fr-fr': 'fr',
|
||||
'vi-vn': 'vi',
|
||||
'it-it': 'it',
|
||||
'cs-cz': 'cs',
|
||||
'pl-pl': 'pl',
|
||||
'nl-nl': 'nl',
|
||||
'pt-pt': 'pt',
|
||||
'zh-cn': 'zh-cn',
|
||||
} as Record<SupportedLocale, ISOLanguage>
|
||||
|
||||
export const DAYJS_LANGUAGE_IMPORTS = {
|
||||
'de-de': () => import('dayjs/locale/de'),
|
||||
'de-swiss': () => import('dayjs/locale/de-at'),
|
||||
'ru-ru': () => import('dayjs/locale/ru'),
|
||||
'fr-fr': () => import('dayjs/locale/fr'),
|
||||
'vi-vn': () => import('dayjs/locale/vi'),
|
||||
'it-it': () => import('dayjs/locale/it'),
|
||||
'cs-cz': () => import('dayjs/locale/cs'),
|
||||
'pl-pl': () => import('dayjs/locale/pl'),
|
||||
'nl-nl': () => import('dayjs/locale/nl'),
|
||||
'pt-pt': () => import('dayjs/locale/pt'),
|
||||
'zh-cn': () => import('dayjs/locale/zh-cn'),
|
||||
} as Record<SupportedLocale, () => Promise<ILocale>>
|
||||
|
||||
export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) {
|
||||
|
||||
const dayjsLanguageLoaded = ref(false)
|
||||
watch(
|
||||
() => i18n.global.locale,
|
||||
async (currentLanguage: string) => {
|
||||
if (!dayjsGlobal) {
|
||||
return
|
||||
}
|
||||
const dayjsLanguageCode = DAYJS_LOCALE_MAPPING[currentLanguage.toLowerCase()] || currentLanguage.toLowerCase()
|
||||
dayjsLanguageLoaded.value = dayjsGlobal.locale() === dayjsLanguageCode
|
||||
if (dayjsLanguageLoaded.value) {
|
||||
return
|
||||
}
|
||||
await DAYJS_LANGUAGE_IMPORTS[currentLanguage.toLowerCase()]()
|
||||
dayjsGlobal.locale(dayjsLanguageCode)
|
||||
dayjsLanguageLoaded.value = true
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
// we export the loading state since that's easier to work with
|
||||
const isLoading = computed(() => !dayjsLanguageLoaded.value)
|
||||
|
||||
return isLoading
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {i18n} from '@/i18n'
|
||||
import { notify } from '@kyvg/vue3-notification'
|
||||
import {notify} from '@kyvg/vue3-notification'
|
||||
|
||||
export const getErrorText = (r) => {
|
||||
|
||||
|
|
|
@ -1,6 +1,28 @@
|
|||
import {SECONDS_A_HOUR} from '@/constants/date'
|
||||
import { REPEAT_TYPES, type IRepeatAfter } from '@/types/IRepeatAfter'
|
||||
import { nativeEnum, number, object, preprocess } from 'zod'
|
||||
|
||||
/**
|
||||
* Parses `repeatAfterSeconds` into a usable js object.
|
||||
*/
|
||||
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
|
||||
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
|
||||
|
||||
// if its dividable by 24, its something with days, otherwise hours
|
||||
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
|
||||
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
|
||||
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
|
||||
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
|
||||
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
|
||||
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
|
||||
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
|
||||
} else {
|
||||
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
|
||||
}
|
||||
}
|
||||
return repeatAfter
|
||||
}
|
||||
|
||||
export const RepeatsSchema = preprocess(
|
||||
(repeats: unknown) => {
|
||||
// Parses the "repeat after x seconds" from the task into a usable js object inside the task.
|
||||
|
@ -9,32 +31,7 @@ export const RepeatsSchema = preprocess(
|
|||
return repeats
|
||||
}
|
||||
|
||||
const repeatAfterHours = (repeats / 60) / 60
|
||||
|
||||
const repeatAfter : IRepeatAfter = {
|
||||
type: 'hours',
|
||||
amount: repeatAfterHours,
|
||||
}
|
||||
|
||||
// if its dividable by 24, its something with days, otherwise hours
|
||||
if (repeatAfterHours % 24 === 0) {
|
||||
const repeatAfterDays = repeatAfterHours / 24
|
||||
if (repeatAfterDays % 7 === 0) {
|
||||
repeatAfter.type = 'weeks'
|
||||
repeatAfter.amount = repeatAfterDays / 7
|
||||
} else if (repeatAfterDays % 30 === 0) {
|
||||
repeatAfter.type = 'months'
|
||||
repeatAfter.amount = repeatAfterDays / 30
|
||||
} else if (repeatAfterDays % 365 === 0) {
|
||||
repeatAfter.type = 'years'
|
||||
repeatAfter.amount = repeatAfterDays / 365
|
||||
} else {
|
||||
repeatAfter.type = 'days'
|
||||
repeatAfter.amount = repeatAfterDays
|
||||
}
|
||||
}
|
||||
|
||||
return repeatAfter
|
||||
return parseRepeatAfter(repeats)
|
||||
},
|
||||
object({
|
||||
type: nativeEnum(REPEAT_TYPES),
|
||||
|
|
|
@ -12,6 +12,8 @@ import type {IRelationKind} from '@/types/IRelationKind'
|
|||
import type {IRepeatAfter} from '@/types/IRepeatAfter'
|
||||
import type {IRepeatMode} from '@/types/IRepeatMode'
|
||||
|
||||
import type {PartialWithId} from '@/types/PartialWithId'
|
||||
|
||||
export interface ITask extends IAbstract {
|
||||
id: number
|
||||
title: string
|
||||
|
@ -34,7 +36,7 @@ export interface ITask extends IAbstract {
|
|||
percentDone: number
|
||||
relatedTasks: Partial<Record<IRelationKind, ITask[]>>
|
||||
attachments: IAttachment[]
|
||||
coverImageAttachmentId: IAttachment['id']
|
||||
coverImageAttachmentId: IAttachment['id'] | null
|
||||
identifier: string
|
||||
index: number
|
||||
isFavorite: boolean
|
||||
|
@ -49,4 +51,6 @@ export interface ITask extends IAbstract {
|
|||
|
||||
listId: IList['id'] // Meta, only used when creating a new task
|
||||
bucketId: IBucket['id']
|
||||
}
|
||||
}
|
||||
|
||||
export type ITaskPartialWithId = PartialWithId<ITask>
|
|
@ -20,4 +20,7 @@ export interface IUser extends IAbstract {
|
|||
created: Date
|
||||
updated: Date
|
||||
settings: IUserSettings
|
||||
|
||||
isLocalUser: boolean
|
||||
deletionScheduledAt: string | Date | null
|
||||
}
|
|
@ -8,6 +8,7 @@ export interface IUserSettings extends IAbstract {
|
|||
discoverableByName: boolean
|
||||
discoverableByEmail: boolean
|
||||
overdueTasksRemindersEnabled: boolean
|
||||
overdueTasksRemindersTime: any
|
||||
defaultListId: undefined | IList['id']
|
||||
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
timezone: string
|
||||
|
|
|
@ -6,7 +6,7 @@ export default class EmailUpdateModel extends AbstractModel<IEmailUpdate> implem
|
|||
newEmail = ''
|
||||
password = ''
|
||||
|
||||
constructor(data : Partial<IEmailUpdate>) {
|
||||
constructor(data : Partial<IEmailUpdate> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export default class PasswordUpdateModel extends AbstractModel<IPasswordUpdate>
|
|||
newPassword = ''
|
||||
oldPassword = ''
|
||||
|
||||
constructor(data: Partial<IPasswordUpdate>) {
|
||||
constructor(data: Partial<IPasswordUpdate> = {}) {
|
||||
super()
|
||||
this.assignData(data)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue