forked from vikunja/frontend
Compare commits
60 Commits
main
...
translatio
Author | SHA1 | Date |
---|---|---|
J. Lavoie | 69590246df | |
J. Lavoie | b203e4a169 | |
J. Lavoie | c5f8ed629a | |
J. Lavoie | 08ed54ab4e | |
kolaente | 2370115c35 | |
kolaente | 86ca6c29c5 | |
Andrey Kashlak | f6b4b44743 | |
Sergio | b54fae513a | |
Jesse James Isler | d350b02aca | |
Jesse James Isler | 1238528e2b | |
Andrey Kashlak | 8de194bc06 | |
Andrey Kashlak | 9800e1701d | |
Andrey Kashlak | 8e6633f70f | |
Andrey Kashlak | eaca985d44 | |
Andrey Kashlak | 1d5def2d8e | |
Andrey Kashlak | 7d5077cd8f | |
Jesse James Isler | b24365640f | |
J. Lavoie | 024af54cd1 | |
Andrey Kashlak | 5cf1fb831a | |
J. Lavoie | eb6ade1fac | |
J. Lavoie | 49fa8dd5ff | |
Allan Nordhøy | 004484fbd7 | |
Luis | 86e9cfdf4b | |
Allan Nordhøy | 32dd9bf138 | |
Allan Nordhøy | 7476949852 | |
Allan Nordhøy | f7e24f9df3 | |
kolaente | 82b756cd99 | |
Allan Nordhøy | 6ade8c6607 | |
Allan Nordhøy | da71cf7220 | |
Allan Nordhøy | 4a7d0d5b7b | |
Allan Nordhøy | 76f67f60bc | |
Allan Nordhøy | 812d1ba560 | |
Allan Nordhøy | aef4792be5 | |
kolaente | e2959f210d | |
kolaente | 33ff902c6c | |
kolaente | fca4b93002 | |
Luis | dc41288ec1 | |
Anonymous | 2fd47b585d | |
Anonymous | 44a4e08d0d | |
Anonymous | db31574858 | |
Anonymous | 345f02b66a | |
Anonymous | 53a4e463f2 | |
Konrad | d3586a3d5c | |
kolaente | 3aa8488dc4 | |
Swann Fournial | b0827e2ba8 | |
Swann Fournial | 4123d739d9 | |
Andrey Kashlak | d55fdbf223 | |
Andrey Kashlak | 2b8884c39a | |
Andrey Kashlak | b93d853022 | |
Andrey Kashlak | 3db06bc81b | |
Andrey Kashlak | 3416c2598e | |
Andrey Kashlak | 2d754f0aac | |
Swann Fournial | 01669831e5 | |
Andrey Kashlak | b25cea2180 | |
Swann Fournial | be86427374 | |
Konrad | 4dbec1acab | |
kolaente | e096de57d3 | |
Swann Fournial | 4ba6261549 | |
Nathan | a707931c55 | |
Andrey Kashlak | 44bdbd2fdb |
129
.drone.yml
129
.drone.yml
|
@ -67,7 +67,7 @@ steps:
|
|||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: lint
|
||||
- name: build
|
||||
image: node:16
|
||||
pull: true
|
||||
environment:
|
||||
|
@ -75,28 +75,7 @@ steps:
|
|||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
commands:
|
||||
- yarn run lint
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
# Building in dev mode to avoid the service worker for testing
|
||||
- name: build-dev
|
||||
image: node:16
|
||||
pull: true
|
||||
environment:
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
commands:
|
||||
- yarn build:dev
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
- name: build-prod
|
||||
image: node:16
|
||||
pull: true
|
||||
environment:
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
commands:
|
||||
- yarn build --dest dist-prod
|
||||
- yarn run build
|
||||
depends_on:
|
||||
- dependencies
|
||||
|
||||
|
@ -109,21 +88,20 @@ steps:
|
|||
- dependencies
|
||||
|
||||
- name: test-frontend
|
||||
image: cypress/browsers:node14.17.0-chrome91-ff89
|
||||
image: cypress/browsers:node12.18.3-chrome87-ff82
|
||||
pull: true
|
||||
environment:
|
||||
CYPRESS_API_URL: http://api:3456/api/v1
|
||||
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
||||
YARN_CACHE_FOLDER: .cache/yarn/
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress/
|
||||
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
|
||||
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 20000
|
||||
commands:
|
||||
- sed -i 's/localhost/api/g' dist-dev/index.html
|
||||
- yarn serve:dist-dev & npx wait-on http://localhost:5000
|
||||
- sed -i 's/localhost/api/g' public/index.html
|
||||
- yarn serve & npx wait-on http://localhost:8080
|
||||
- yarn test:frontend --browser chrome
|
||||
depends_on:
|
||||
- dependencies
|
||||
- build-dev
|
||||
|
||||
- name: upload-test-results
|
||||
image: plugins/s3:1
|
||||
|
@ -310,7 +288,7 @@ trigger:
|
|||
- push
|
||||
|
||||
depends_on:
|
||||
- release-latest
|
||||
- release-latest
|
||||
|
||||
steps:
|
||||
- name: trigger
|
||||
|
@ -341,7 +319,7 @@ trigger:
|
|||
- "refs/tags/**"
|
||||
|
||||
steps:
|
||||
- name: docker-unstable
|
||||
- name: docker-latest
|
||||
image: plugins/docker:linux-arm
|
||||
pull: true
|
||||
settings:
|
||||
|
@ -350,7 +328,7 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/frontend
|
||||
tags: unstable-linux-arm
|
||||
tags: latest-linux-arm
|
||||
build_args:
|
||||
- USE_RELEASE=true
|
||||
- RELEASE_VERSION=unstable
|
||||
|
@ -380,7 +358,7 @@ steps:
|
|||
depends_on:
|
||||
- clone
|
||||
|
||||
- name: docker-unstable-arm64
|
||||
- name: docker-latest-arm64
|
||||
image: plugins/docker:linux-arm64
|
||||
pull: true
|
||||
settings:
|
||||
|
@ -389,7 +367,7 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/frontend
|
||||
tags: unstable-linux-arm64
|
||||
tags: latest-linux-arm64
|
||||
build_args:
|
||||
- USE_RELEASE=true
|
||||
- RELEASE_VERSION=unstable
|
||||
|
@ -438,7 +416,7 @@ trigger:
|
|||
- "refs/tags/**"
|
||||
|
||||
steps:
|
||||
- name: docker-unstable
|
||||
- name: docker-latest
|
||||
image: plugins/docker:linux-amd64
|
||||
pull: true
|
||||
settings:
|
||||
|
@ -447,7 +425,7 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/frontend
|
||||
tags: unstable-linux-amd64
|
||||
tags: latest-linux-amd64
|
||||
build_args:
|
||||
- USE_RELEASE=true
|
||||
- RELEASE_VERSION=unstable
|
||||
|
@ -488,12 +466,12 @@ depends_on:
|
|||
- docker-arm-release
|
||||
|
||||
steps:
|
||||
- name: manifest-unstable
|
||||
- name: manifest-latest
|
||||
pull: always
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
tags: unstable
|
||||
spec: docker-manifest-unstable.tmpl
|
||||
tags: latest
|
||||
spec: docker-manifest-latest.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
|
@ -516,23 +494,6 @@ steps:
|
|||
when:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
- name: manifest-release-latest
|
||||
pull: always
|
||||
image: plugins/manifest
|
||||
depends_on:
|
||||
- clone
|
||||
settings:
|
||||
tags: latest
|
||||
ignore_missing: true
|
||||
spec: docker-manifest.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
when:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
|
@ -569,8 +530,7 @@ steps:
|
|||
- failure
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: update-translations
|
||||
name: ping-weblate
|
||||
|
||||
depends_on:
|
||||
- build
|
||||
|
@ -582,51 +542,20 @@ trigger:
|
|||
- push
|
||||
|
||||
steps:
|
||||
- name: download
|
||||
pull: always
|
||||
image: jonasfranz/crowdin
|
||||
settings:
|
||||
download: true
|
||||
export_dir: src/i18n/lang/
|
||||
ignore_branch: true
|
||||
project_identifier: vikunja
|
||||
environment:
|
||||
CROWDIN_KEY:
|
||||
from_secret: crowdin_key
|
||||
|
||||
- name: move-files
|
||||
pull: always
|
||||
image: bash
|
||||
depends_on:
|
||||
- download
|
||||
commands:
|
||||
- mv src/i18n/lang/*/*.json src/i18n/lang
|
||||
|
||||
- name: push
|
||||
pull: always
|
||||
- name: update-translation-base
|
||||
image: appleboy/drone-git-push
|
||||
depends_on:
|
||||
- move-files
|
||||
failure: ignore
|
||||
settings:
|
||||
author_email: "frederik@vikunja.io"
|
||||
author_name: Frederick [Bot]
|
||||
branch: main
|
||||
commit: true
|
||||
commit_message: "[skip ci] Updated translations via Crowdin"
|
||||
remote: "ssh://git@kolaente.dev:9022/vikunja/frontend.git"
|
||||
branch: translations
|
||||
remote: ssh://git@kolaente.dev:9022/vikunja/frontend.git
|
||||
ssh_key:
|
||||
from_secret: translation_git_push_ssh_key
|
||||
|
||||
- name: upload
|
||||
pull: always
|
||||
image: jonasfranz/crowdin
|
||||
from_secret: translations_branch_update_ssh_key
|
||||
- name: notify-weblate
|
||||
image: curlimages/curl
|
||||
depends_on:
|
||||
- clone
|
||||
settings:
|
||||
files:
|
||||
en.json: src/i18n/lang/en.json
|
||||
ignore_branch: true
|
||||
project_identifier: vikunja
|
||||
- update-translation-base
|
||||
environment:
|
||||
CROWDIN_KEY:
|
||||
from_secret: crowdin_key
|
||||
WEBLATE_TOKEN:
|
||||
from_secret: weblate_token
|
||||
commands:
|
||||
- ./ping-weblate.sh
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
|
||||
[*.vue]
|
||||
indent_style = tab
|
||||
|
||||
[*.{yaml,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -1,6 +1,6 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist*
|
||||
/dist
|
||||
*.zip
|
||||
|
||||
# local env files
|
||||
|
@ -11,7 +11,6 @@ node_modules
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
stats.html
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
|
@ -21,7 +20,6 @@ stats.html
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
!rollup.sw.js
|
||||
|
||||
# Test files
|
||||
cypress/screenshots
|
||||
|
|
346
CHANGELOG.md
346
CHANGELOG.md
|
@ -2,347 +2,13 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
|
||||
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
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.18.1] - 2021-09-08
|
||||
|
||||
### Added
|
||||
|
||||
* feat: make it possible to fake online state via dev env (#720)
|
||||
|
||||
### Fixed
|
||||
|
||||
* fix: call to /null from background image (#714)
|
||||
* Fix data export download progress
|
||||
* fix: kanban-card mutatation violation (#712)
|
||||
* Fix missing translation when creating a new task on the kanban board
|
||||
* Fix rearranging tasks in a kanban bucket when its limit was reached
|
||||
* Fix sort order for table view
|
||||
* Fix task attributes overridden when saving the task title with enter
|
||||
* Fix translation badge
|
||||
|
||||
### Dependency Updates
|
||||
|
||||
* Update dependency @4tw/cypress-drag-drop to v2 (#711)
|
||||
* Update dependency axios to v0.21.4 (#705)
|
||||
* Update dependency jest to v27.1.1 (#716)
|
||||
* Update dependency vite-plugin-vue2 to v1.8.2 (#707)
|
||||
* Update dependency vite to v2.5.4 (#708)
|
||||
* Update dependency vite to v2.5.5 (#709)
|
||||
* Update typescript-eslint monorepo to v4.31.0 (#706)
|
||||
|
||||
|
||||
## [0.18.0] - 2021-09-05
|
||||
|
||||
### Added
|
||||
|
||||
* Add a button to copy an attachment url from the attachment overview
|
||||
* Add collapsing kanban buckets
|
||||
* Add confirm with enter when setting a new password
|
||||
* Add default list setting & creating tasks from home (#520)
|
||||
* Add depends_on for push step
|
||||
* Add depends_on for upload step
|
||||
* Add drag delay on mobile
|
||||
* Add express for serve:dev
|
||||
* Add filters for quick action bar
|
||||
* Add frontend tests for list history
|
||||
* Add making tasks favorite from the task detail view
|
||||
* Add missing position property to list and bucket models
|
||||
* Add more debug logs for gantt charts
|
||||
* Add more global state tests (#521)
|
||||
* Add proofread languages to available languages
|
||||
* Add quick action bar shortcut to shortcut overview
|
||||
* Add setting for the first day of the week
|
||||
* Add showing version info in GUI
|
||||
* Add syncing translations to crowdin
|
||||
* Add timeout to fix race condition when authenticating as a link share and renewing the token simultaneously
|
||||
* Add translations (#562)
|
||||
* Add typescript support for helper functions (#598)
|
||||
* Add vite (#416)
|
||||
* Allow failure of the weblate update step
|
||||
* Always set the kanban board to full width for share links
|
||||
* Another day, another js date edge-case
|
||||
* Automatically update approved translations from crowdin
|
||||
* Break long list titles in list overview
|
||||
* Preload labels and use locally stored in vuex
|
||||
* PWA improvments (#622)
|
||||
* Quick Actions & global search (#528)
|
||||
* Quick add magic for tasks (#570)
|
||||
* Reorder tasks, lists and kanban buckets (#620)
|
||||
* Show last visited list on home page
|
||||
* Show recently visited lists in quick actions
|
||||
* Show salutation based on the time of day
|
||||
* Sort labels alphabetically on tasks
|
||||
* Switch the :latest docker image tag to contain the latest release instead of the latest unstable
|
||||
|
||||
### Changed
|
||||
|
||||
* Change building latest docker image
|
||||
* Change desktop downstream trigger plugin with our own debug build
|
||||
* Change menu hamburger icon
|
||||
* Change quick add magic characters to be more familiar with the todoist ones
|
||||
* Change the docker builder image to a working one on arm
|
||||
* chore: discard old font file formats (#673)
|
||||
* chore: only import common languages (#671)
|
||||
* Cleanup broken sw functions
|
||||
* Cleanup drone pipeline
|
||||
* Cleanup old vue cli config
|
||||
* Configure tests retries
|
||||
* Decrease page padding on task detail page
|
||||
* Directly redirect to the openid auth provider if that's the only auth method
|
||||
* Don't allow dragging a list when the user does not have the rights
|
||||
* Don't load already loaded task attachments again when saving an edited task description
|
||||
* Don't prefetch all i18n files
|
||||
* Don't show archived lists/namespaces in quick actions
|
||||
* feat: provide global variables in all components (#669)
|
||||
* Hide favorite list edit menu
|
||||
* Hide keyboard shortcuts indicator on mobile
|
||||
* Improve chunk size
|
||||
* Improve some translations (#581)
|
||||
* Improve tests
|
||||
* Indicate done tasks in quick actions
|
||||
* Load list background in list card
|
||||
* Make editor edit button at the bottom the default and make sure the done button stands out more
|
||||
* Make saving a text edit a button
|
||||
* Make sure highlight.js is always lazy-loaded
|
||||
* Make sure the task popup view takes up all the space it can on mobile
|
||||
* Make tests less flaky
|
||||
* Make the logo smaller on link shared lists
|
||||
* Make the progress bar color lighter
|
||||
* Move creation of new items to the bottom of the multiselect list
|
||||
* Move general settings to the top
|
||||
* Move translated files after downloading them
|
||||
* Move weblate ping to shell script
|
||||
* Only add a drag delay if on mobile instead of setting it to 0
|
||||
* Only build a bundle for modern browsers
|
||||
* Refactor success and error messages
|
||||
* Refactor success and error notifications to prevent html in them
|
||||
* Remove logout button for link shares
|
||||
* Run frontend-tests with dist in ci (#605)
|
||||
* Save auth tokens from link shares only in memory, don't persist them to localStorage
|
||||
* Search namespaces locally only when duplicating a list
|
||||
* Show errors from openid provider
|
||||
* Show labels alphabetically sorted in the overview
|
||||
* Small cleanups & code improvements
|
||||
* TOTP UX improvements & translation fixes
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix changing the repeat mode of a task when no value is entered yet
|
||||
* Fix comment on different task after clicking on a task notification
|
||||
* Fix CTA spacings
|
||||
* Fix date parsing parsing words with weekdays in them (#607)
|
||||
* fix(deps): update dependency marked to v3.0.1 (#677)
|
||||
* fix(deps): update dependency marked to v3.0.2 (#682)
|
||||
* Fix error property already defined as a function
|
||||
* Fix flickering pre-loaded search results when focusing the search input
|
||||
* Fix Gantt layout overflowsing on mobile
|
||||
* Fix gantt months being wrong
|
||||
* Fix git push remote to update crowdin translations
|
||||
* Fix global mutation of has tasks state
|
||||
* Fix header layout for long list titles
|
||||
* Fix highlight.js in editor
|
||||
* Fix home page tests
|
||||
* Fix keyboard shortcuts not working on the task detail page
|
||||
* Fix label changes appearing to be saved immediately when editing them
|
||||
* Fix labels list in saved filter spacing
|
||||
* Fix lint
|
||||
* Fix list archived notification mobile layout
|
||||
* Fix list settings not being available when list backgrounds are disabled
|
||||
* Fix lists showing up multiple times in history
|
||||
* Fix llama background url
|
||||
* Fix loading a list when it was already partially saved in vuex
|
||||
* Fix loading & disabled state on inputs when creating a new task
|
||||
* Fix loading labels when editing a saved filter
|
||||
* Fix menu styles
|
||||
* Fix missing background for tasks on a shared list with a background
|
||||
* Fix multiselect search padding
|
||||
* Fix new lists created with quick actions not showing up in the menu
|
||||
* fix: non unique ids (#672)
|
||||
* Fix not reloading tasks of a saved filter after editing it
|
||||
* Fix not updating list name in store when changing it
|
||||
* Fix other values getting pushed away when creating a new one through multiselect
|
||||
* Fix padding for kanban cards
|
||||
* Fix parsing dates on the last day of the month
|
||||
* Fix populating task details ater updating the description
|
||||
* Fix quick actions not opening
|
||||
* Fix quick actions not working when nonexisting lists where left over in history
|
||||
* Fix redirecting to /login for some routes
|
||||
* Fix removing a namespace from state after it was deleted
|
||||
* Fix resetting date filters from upcoming after viewing a task detail page (popup)
|
||||
* Fix sass division
|
||||
* Fix saving showing archived setting
|
||||
* Fix selecting a single value from multiselect
|
||||
* Fix sending openid scopes when authenticating
|
||||
* Fix sending the user back to the list view they came from when opening a task in detail view
|
||||
* Fix setting a task as favorite button
|
||||
* Fix setting delete button for newly created task comments
|
||||
* Fix setting filters for reminders
|
||||
* Fix setting secret for updating translations
|
||||
* Fix setting task favorite status in test fixtures
|
||||
* Fix showing an editor save button in cases where it wasn't required
|
||||
* Fix showing edit buttons when the user does not have the rights to use them
|
||||
* Fix showing import tasks cta when tasks are loading
|
||||
* Fix some translation strings
|
||||
* Fix sorting labels
|
||||
* Fix spacing for task detail view in lists with a background
|
||||
* Fix table headers wrapping in table view
|
||||
* Fix table text alignment in task detail page
|
||||
* Fix table view scrolling on mobile
|
||||
* Fix test for saving a task description
|
||||
* Fix tests failing on thursdays
|
||||
* Fix token in storage not getting renewed
|
||||
* Fix translating dates
|
||||
* Fix usage of / in sass
|
||||
* Fix user name and avatar alignment in navbar
|
||||
* Fix users not removed from the list in settings when unshared
|
||||
* Fix user test fixtures
|
||||
* fix: vuex mutation violation from draggable (#674)
|
||||
|
||||
### Dependency Updates
|
||||
|
||||
* chore(deps): update dependency @4tw/cypress-drag-drop to v1.8.1 (#693)
|
||||
* chore(deps): update dependency autoprefixer to v10.3.3 (#684)
|
||||
* chore(deps): update dependency autoprefixer to v10.3.4 (#697)
|
||||
* chore(deps): update dependency axios to v0.21.2 (#698)
|
||||
* chore(deps): update dependency axios to v0.21.3 (#700)
|
||||
* chore(deps): update dependency cypress to v8.3.1 (#689)
|
||||
* chore(deps): update dependency esbuild to v0.12.23 (#683)
|
||||
* chore(deps): update dependency esbuild to v0.12.24 (#688)
|
||||
* chore(deps): update dependency esbuild to v0.12.25 (#696)
|
||||
* chore(deps): update dependency eslint-plugin-vue to v7.17.0 (#686)
|
||||
* chore(deps): update dependency jest to v27.1.0 (#687)
|
||||
* chore(deps): update dependency sass to v1.38.1 (#679)
|
||||
* chore(deps): update dependency sass to v1.38.2 (#690)
|
||||
* chore(deps): update dependency sass to v1.39.0 (#695)
|
||||
* chore(deps): update dependency typescript to v4.4.2 (#685)
|
||||
* chore(deps): update dependency vite-plugin-pwa to v0.11.2 (#681)
|
||||
* chore(deps): update dependency vite to v2.5.1 (#680)
|
||||
* chore(deps): update dependency vite to v2.5.2 (#692)
|
||||
* chore(deps): update dependency vite to v2.5.3 (#694)
|
||||
* chore(deps): update typescript-eslint monorepo to v4.29.3 (#676)
|
||||
* chore(deps): update typescript-eslint monorepo to v4.30.0 (#691)
|
||||
* Update dependency autoprefixer to v10.3.2 (#670)
|
||||
* Update dependency browserslist to v4.16.7 (#634)
|
||||
* Update dependency browserslist to v4.16.8 (#664)
|
||||
* Update dependency browserslist to v4.17.0 (#701)
|
||||
* Update dependency bulma to v0.9.3 (#554)
|
||||
* Update dependency cypress-file-upload to v5.0.8 (#556)
|
||||
* Update dependency cypress to v7.3.0 (#507)
|
||||
* Update dependency cypress to v7.4.0 (#517)
|
||||
* Update dependency cypress to v7.5.0 (#541)
|
||||
* Update dependency cypress to v7.6.0 (#561)
|
||||
* Update dependency cypress to v7.7.0 (#577)
|
||||
* Update dependency cypress to v8.1.0 (#624)
|
||||
* Update dependency cypress to v8.2.0 (#637)
|
||||
* Update dependency cypress to v8.3.0 (#660)
|
||||
* Update dependency cypress to v8 (#601)
|
||||
* Update dependency date-fns to v2.22.0 (#523)
|
||||
* Update dependency date-fns to v2.22.1 (#524)
|
||||
* Update dependency date-fns to v2.23.0 (#604)
|
||||
* Update dependency dompurify to v2.2.9 (#529)
|
||||
* Update dependency dompurify to v2.3.0 (#573)
|
||||
* Update dependency dompurify to v2.3.1 (#655)
|
||||
* Update dependency esbuild to v0.12.15 (#610)
|
||||
* Update dependency esbuild to v0.12.16 (#614)
|
||||
* Update dependency esbuild to v0.12.17 (#623)
|
||||
* Update dependency esbuild to v0.12.18 (#638)
|
||||
* Update dependency esbuild to v0.12.19 (#643)
|
||||
* Update dependency esbuild to v0.12.20 (#654)
|
||||
* Update dependency esbuild to v0.12.21 (#666)
|
||||
* Update dependency esbuild to v0.12.22 (#668)
|
||||
* Update dependency eslint-plugin-vue to v7.10.0 (#525)
|
||||
* Update dependency eslint-plugin-vue to v7.11.0 (#547)
|
||||
* Update dependency eslint-plugin-vue to v7.11.1 (#548)
|
||||
* Update dependency eslint-plugin-vue to v7.12.1 (#565)
|
||||
* Update dependency eslint-plugin-vue to v7.13.0 (#574)
|
||||
* Update dependency eslint-plugin-vue to v7.14.0 (#597)
|
||||
* Update dependency eslint-plugin-vue to v7.15.0 (#625)
|
||||
* Update dependency eslint-plugin-vue to v7.15.1 (#633)
|
||||
* Update dependency eslint-plugin-vue to v7.16.0 (#648)
|
||||
* Update dependency eslint to v7.27.0 (#514)
|
||||
* Update dependency eslint to v7.28.0 (#539)
|
||||
* Update dependency eslint to v7.29.0 (#555)
|
||||
* Update dependency eslint to v7.30.0 (#571)
|
||||
* Update dependency eslint to v7.31.0 (#596)
|
||||
* Update dependency eslint to v7.32.0 (#627)
|
||||
* Update dependency highlight.js to v11.0.1 (#538)
|
||||
* Update dependency highlight.js to v11.1.0 (#582)
|
||||
* Update dependency highlight.js to v11.2.0 (#630)
|
||||
* Update dependency highlight.js to v11 (#527)
|
||||
* Update dependency jest to v27.0.3 (#526)
|
||||
* Update dependency jest to v27.0.4 (#535)
|
||||
* Update dependency jest to v27.0.5 (#558)
|
||||
* Update dependency jest to v27.0.6 (#569)
|
||||
* Update dependency jest to v27 (#519)
|
||||
* Update dependency marked to v2.0.4 (#510)
|
||||
* Update dependency marked to v2.0.5 (#513)
|
||||
* Update dependency marked to v2.0.6 (#522)
|
||||
* Update dependency marked to v2.0.7 (#532)
|
||||
* Update dependency marked to v2.1.0 (#552)
|
||||
* Update dependency marked to v2.1.1 (#553)
|
||||
* Update dependency marked to v2.1.2 (#559)
|
||||
* Update dependency marked to v2.1.3 (#567)
|
||||
* Update dependency marked to v3 (#657)
|
||||
* Update dependency @rollup/plugin-commonjs to v19.0.2 (#617)
|
||||
* Update dependency sass to v1.33.0 (#512)
|
||||
* Update dependency sass to v1.34.0 (#515)
|
||||
* Update dependency sass to v1.34.1 (#534)
|
||||
* Update dependency sass to v1.35.0 (#550)
|
||||
* Update dependency sass to v1.35.1 (#551)
|
||||
* Update dependency sass to v1.35.2 (#579)
|
||||
* Update dependency sass to v1.36.0 (#606)
|
||||
* Update dependency sass to v1.37.0 (#628)
|
||||
* Update dependency sass to v1.37.2 (#632)
|
||||
* Update dependency sass to v1.37.5 (#635)
|
||||
* Update dependency sass to v1.38.0 (#661)
|
||||
* Update dependency ts-jest to v27.0.4 (#602)
|
||||
* Update dependency ts-jest to v27.0.5 (#662)
|
||||
* Update dependency @types/jest to v27.0.1 (#653)
|
||||
* Update dependency @types/jest to v27 (#650)
|
||||
* Update dependency vite-plugin-pwa to v0.10.0 (#644)
|
||||
* Update dependency vite-plugin-pwa to v0.11.0 (#667)
|
||||
* Update dependency vite-plugin-pwa to v0.8.2 (#612)
|
||||
* Update dependency vite-plugin-pwa to v0.9.3 (#629)
|
||||
* Update dependency vite-plugin-vue2 to v1.7.3 (#613)
|
||||
* Update dependency vite-plugin-vue2 to v1.8.0 (#646)
|
||||
* Update dependency vite-plugin-vue2 to v1.8.1 (#656)
|
||||
* Update dependency vite to v2.4.3 (#611)
|
||||
* Update dependency vite to v2.4.4 (#619)
|
||||
* Update dependency vite to v2.5.0 (#658)
|
||||
* Update dependency vue-advanced-cropper to v1.6.0 (#516)
|
||||
* Update dependency vue-advanced-cropper to v1.7.0 (#543)
|
||||
* Update dependency vue-advanced-cropper to v1.8.0 (#641)
|
||||
* Update dependency vue-advanced-cropper to v1.8.1 (#642)
|
||||
* Update dependency vue-advanced-cropper to v1.8.2 (#645)
|
||||
* Update dependency vue-flatpickr-component to v8.1.7 (#572)
|
||||
* Update dependency vue-i18n to v8.24.5 (#564)
|
||||
* Update dependency vue-i18n to v8.25.0 (#595)
|
||||
* Update dependency vue-router to v3.5.2 (#557)
|
||||
* Update dependency wait-on to v6 (#568)
|
||||
* Update dependency workbox-cli to v6.1.5 (#609)
|
||||
* Update Font Awesome (#636)
|
||||
* Update Node.js (#549)
|
||||
* Update Node.js to v16.4.1 (#576)
|
||||
* Update Node.js to v16.4.2 (#578)
|
||||
* Update typescript-eslint monorepo to v4.28.4 (#600)
|
||||
* Update typescript-eslint monorepo to v4.28.5 (#618)
|
||||
* Update typescript-eslint monorepo to v4.29.0 (#631)
|
||||
* Update typescript-eslint monorepo to v4.29.1 (#647)
|
||||
* Update typescript-eslint monorepo to v4.29.2 (#659)
|
||||
* Update vue monorepo to v2.6.13 (#530)
|
||||
* Update vue monorepo to v2.6.14 (#540)
|
||||
* Update workbox monorepo to v6.2.0 (#639)
|
||||
* Update workbox monorepo to v6.2.2 (#640)
|
||||
* Update workbox monorepo to v6.2.4 (#649)
|
||||
* User account deletion (#651)
|
||||
* User Data Export and import (#699)
|
||||
|
||||
## [0.17.0 - 2021-05-14]
|
||||
|
||||
### Added
|
||||
|
@ -482,8 +148,7 @@ The releases aim at the api versions which is why there are missing versions.
|
|||
* Make sure all arm64 build steps run in parallel
|
||||
* Make sure all empty pages have a call to action
|
||||
* Make sure all popups & dropdowns are animated
|
||||
* Make sure attachements are only added once to the list after uploading + Make sure the attachment list shows up every
|
||||
time after adding an attachment
|
||||
* Make sure attachements are only added once to the list after uploading + Make sure the attachment list shows up every time after adding an attachment
|
||||
* Make sure no cta's are visible while the page is loading
|
||||
* Make sure the loading spinner is always visible at the end of the page
|
||||
* Make the button shadow lighter
|
||||
|
@ -1010,7 +675,7 @@ The releases aim at the api versions which is why there are missing versions.
|
|||
* Hide totp settings if it is disabled server side
|
||||
* Increase network timeout when building docker image
|
||||
* Make sure the version includes the tag when building docker images
|
||||
* # PrideMonth
|
||||
* #PrideMonth
|
||||
* Only renew user token on tab focus events
|
||||
* Redirect the user to login page if the token expired when the tab gets focus again
|
||||
* Remove title length restrictions
|
||||
|
@ -1045,7 +710,7 @@ The releases aim at the api versions which is why there are missing versions.
|
|||
|
||||
## [0.13] - 2020-05-12
|
||||
|
||||
#### Added
|
||||
#### Added
|
||||
|
||||
* Add docker run script to change api url on startup
|
||||
* Add github token for renovate (#89)
|
||||
|
@ -1390,7 +1055,6 @@ The releases aim at the api versions which is why there are missing versions.
|
|||
* Use email instead of username when resetting a password
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed trying to verify an email when there was none
|
||||
* Fixed loading tasks when the user was not authenticated
|
||||
|
||||
|
|
16
README.md
16
README.md
|
@ -4,8 +4,8 @@
|
|||
|
||||
[![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.18.1-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||
[![Download](https://img.shields.io/badge/download-v0.17.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Translation](https://hosted.weblate.org/widgets/vikunja/-/frontend/svg-badge.svg)](https://hosted.weblate.org/engage/vikunja/)
|
||||
|
||||
This is the web frontend for Vikunja, written in Vue.js.
|
||||
|
||||
|
@ -20,25 +20,21 @@ If you find any security-related issues you don't want to disclose publicly, ple
|
|||
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
|
||||
|
||||
## Project setup
|
||||
|
||||
```shell
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
|
||||
```shell
|
||||
```
|
||||
yarn run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
```shell
|
||||
```
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
|
||||
```shell
|
||||
```
|
||||
yarn run lint
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app',
|
||||
],
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"baseUrl": "http://localhost:5000",
|
||||
"baseUrl": "http://localhost:8080",
|
||||
"env": {
|
||||
"API_URL": "http://localhost:3456/api/v1",
|
||||
"TEST_SECRET": "testingS3cr3et"
|
||||
|
|
|
@ -14,6 +14,7 @@ export class TaskFactory extends Factory {
|
|||
done: false,
|
||||
list_id: 1,
|
||||
created_by_id: 1,
|
||||
is_favorite: false,
|
||||
index: '{increment}',
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
|
|
|
@ -13,7 +13,7 @@ export class UserFactory extends Factory {
|
|||
id: '{increment}',
|
||||
username: faker.lorem.word(10) + faker.random.uuid(),
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
|
||||
status: 0,
|
||||
is_active: true,
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now)
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ describe('Lists', () => {
|
|||
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content')
|
||||
.contains('Edit')
|
||||
.click()
|
||||
cy.get('#title')
|
||||
cy.get('#listtext')
|
||||
.type(`{selectall}${newListName}`)
|
||||
cy.get('footer.modal-card-foot .button')
|
||||
.contains('Save')
|
||||
|
@ -253,11 +253,11 @@ describe('Lists', () => {
|
|||
|
||||
describe('Gantt View', () => {
|
||||
it('Hides tasks with no dates', () => {
|
||||
const tasks = TaskFactory.create(1)
|
||||
TaskFactory.create(1)
|
||||
cy.visit('/lists/1/gantt')
|
||||
|
||||
cy.get('.gantt-chart-container .gantt-chart .tasks')
|
||||
.should('not.contain', tasks[0].title)
|
||||
.should('be.empty')
|
||||
})
|
||||
|
||||
it('Shows tasks from the current and next month', () => {
|
||||
|
@ -436,23 +436,26 @@ describe('Lists', () => {
|
|||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can drag tasks around', () => {
|
||||
const tasks = TaskFactory.create(2, {
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.get('.kanban .bucket .tasks .task')
|
||||
.contains(tasks[0].title)
|
||||
.first()
|
||||
.drag('.kanban .bucket:nth-child(2) .tasks .dropper div')
|
||||
|
||||
cy.get('.kanban .bucket:nth-child(2) .tasks')
|
||||
.should('contain', tasks[0].title)
|
||||
cy.get('.kanban .bucket:nth-child(1) .tasks')
|
||||
.should('not.contain', tasks[0].title)
|
||||
})
|
||||
// The following test does not work. It seems like vue-smooth-dnd does not use either mousemove or dragstart
|
||||
// (not sure why this actually works at all?) and as I'm planning to swap that out for vuedraggable/sortable.js
|
||||
// anyway, I figured it wouldn't be worth the hassle right now.
|
||||
|
||||
// it('Can drag tasks around', () => {
|
||||
// const tasks = TaskFactory.create(2, {
|
||||
// list_id: 1,
|
||||
// bucket_id: 1,
|
||||
// })
|
||||
// cy.visit('/lists/1/kanban')
|
||||
//
|
||||
// cy.get('.kanban .bucket .tasks .task')
|
||||
// .contains(tasks[0].title)
|
||||
// .first()
|
||||
// .drag('.kanban .bucket:nth-child(2) .tasks .smooth-dnd-container.vertical')
|
||||
// .trigger('mousedown', {which: 1})
|
||||
// .trigger('mousemove', {clientX: 500, clientY: 0})
|
||||
// .trigger('mouseup', {force: true})
|
||||
// })
|
||||
|
||||
it('Should navigate to the task when the task card is clicked', () => {
|
||||
const tasks = TaskFactory.create(5, {
|
||||
|
|
|
@ -169,7 +169,7 @@ describe('Task', () => {
|
|||
cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
|
||||
.type('{selectall}New Description')
|
||||
cy.get('.task-view .details.content.description .editor a')
|
||||
.contains('Save')
|
||||
.contains('Done')
|
||||
.click()
|
||||
|
||||
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('User Settings', () => {
|
|||
.contains('Save')
|
||||
.click()
|
||||
|
||||
cy.wait(3000) // Wait for the request to finish
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.navbar .user .username')
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import './commands'
|
||||
import 'cypress-file-upload'
|
||||
import '@4tw/cypress-drag-drop'
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
image: vikunja/frontend:unstable
|
||||
image: vikunja/frontend:latest
|
||||
manifests:
|
||||
-
|
||||
image: vikunja/frontend:unstable-linux-amd64
|
||||
image: vikunja/frontend:latest-linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/frontend:unstable-linux-arm64
|
||||
image: vikunja/frontend:latest-linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/frontend:unstable-linux-arm
|
||||
image: vikunja/frontend:latest-linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
37
index.html
37
index.html
|
@ -1,37 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Vikunja</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="Vikunja (/vɪˈkuːnjə/) - The to-do app to organize your life.">
|
||||
<meta name="theme-color" content="#1973ff"/>
|
||||
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/images/icons/apple-touch-icon-180x180.png"/>
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700italic.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-italic.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-300.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-500.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-700.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-regular.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-regular.woff2" as="font">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but Vikunja doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<script>
|
||||
//
|
||||
// This variable points the frontend to the api.
|
||||
// It has to be the full url, including the last /api/v1 part and port.
|
||||
// You can change this if your api is not reachable on the same port as the frontend.
|
||||
window.API_URL = 'http://localhost:3456/api/v1'
|
||||
//
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
122
package.json
122
package.json
|
@ -3,76 +3,62 @@
|
|||
"version": "0.10.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"serve:dist-dev": "node scripts/serve-dist.js",
|
||||
"serve:dist": "vite preview",
|
||||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:dev": "vite build -m development --outDir dist-dev/",
|
||||
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
|
||||
"serve": "vue-cli-service serve",
|
||||
"serve:dist": "node scripts/serve-dist.js",
|
||||
"build": "vue-cli-service build --modern",
|
||||
"build:report": "vue-cli-service build --report",
|
||||
"lint": "vue-cli-service lint --ignore-pattern '*.test.*'",
|
||||
"cypress:open": "cypress open",
|
||||
"test:unit": "jest",
|
||||
"test:frontend": "cypress run"
|
||||
},
|
||||
"dependencies": {
|
||||
"browserslist": "4.17.0",
|
||||
"browserslist": "4.16.6",
|
||||
"bulma": "0.9.3",
|
||||
"camel-case": "4.1.2",
|
||||
"copy-to-clipboard": "3.3.1",
|
||||
"date-fns": "2.23.0",
|
||||
"dompurify": "2.3.1",
|
||||
"highlight.js": "11.2.0",
|
||||
"is-touch-device": "1.0.1",
|
||||
"date-fns": "2.22.1",
|
||||
"dompurify": "2.3.0",
|
||||
"highlight.js": "11.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "3.0.3",
|
||||
"marked": "2.1.3",
|
||||
"register-service-worker": "1.7.2",
|
||||
"sass": "1.35.2",
|
||||
"snake-case": "3.0.4",
|
||||
"verte": "0.0.12",
|
||||
"vue": "2.6.14",
|
||||
"vue-advanced-cropper": "1.8.2",
|
||||
"vue-advanced-cropper": "1.7.0",
|
||||
"vue-drag-resize": "1.5.4",
|
||||
"vue-easymde": "1.4.0",
|
||||
"vue-i18n": "8.25.0",
|
||||
"vue-i18n": "8.24.5",
|
||||
"vue-shortkey": "3.1.7",
|
||||
"vuedraggable": "2.24.3",
|
||||
"vuex": "3.6.2",
|
||||
"workbox-precaching": "6.3.0"
|
||||
"vue-smooth-dnd": "0.8.1",
|
||||
"vuex": "3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/vue-fontawesome": "2.0.2",
|
||||
"@types/jest": "27.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.31.0",
|
||||
"@typescript-eslint/parser": "4.31.0",
|
||||
"@vue/babel-preset-app": "4.5.13",
|
||||
"@vue/eslint-config-typescript": "7.0.0",
|
||||
"autoprefixer": "10.3.4",
|
||||
"axios": "0.21.4",
|
||||
"@vue/cli": "4.5.13",
|
||||
"@vue/cli-plugin-babel": "4.5.13",
|
||||
"@vue/cli-plugin-eslint": "4.5.13",
|
||||
"@vue/cli-plugin-pwa": "4.5.13",
|
||||
"@vue/cli-service": "4.5.13",
|
||||
"axios": "0.21.1",
|
||||
"babel-eslint": "10.1.0",
|
||||
"cypress": "8.3.1",
|
||||
"cypress": "7.7.0",
|
||||
"cypress-file-upload": "5.0.8",
|
||||
"esbuild": "0.12.26",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-vue": "7.17.0",
|
||||
"express": "4.17.1",
|
||||
"eslint": "7.30.0",
|
||||
"eslint-plugin-vue": "7.13.0",
|
||||
"faker": "5.5.3",
|
||||
"jest": "27.1.1",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.5.2",
|
||||
"sass": "1.39.2",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.4.2",
|
||||
"vite": "2.5.6",
|
||||
"vite-plugin-pwa": "0.11.2",
|
||||
"vite-plugin-vue2": "1.8.2",
|
||||
"jest": "27.0.6",
|
||||
"sass-loader": "10.2.0",
|
||||
"vue-flatpickr-component": "8.1.7",
|
||||
"vue-notification": "1.3.20",
|
||||
"vue-router": "3.5.2",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"wait-on": "6.0.0",
|
||||
"workbox-cli": "6.3.0"
|
||||
"wait-on": "6.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
@ -81,32 +67,14 @@
|
|||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript"
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {
|
||||
"vue/html-quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
},
|
||||
"rules": {},
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser"
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"*.test.*",
|
||||
"*.test.js",
|
||||
"cypress/*"
|
||||
]
|
||||
},
|
||||
|
@ -118,27 +86,13 @@
|
|||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie > 0",
|
||||
"not dead",
|
||||
"Firefox ESR"
|
||||
"not ie < 11"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"jest": {
|
||||
"testPathIgnorePatterns": [
|
||||
"cypress"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"preset": "ts-jest",
|
||||
"roots": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(js|tsx?)$": "ts-jest"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js",
|
||||
"json"
|
||||
]
|
||||
},
|
||||
"license": "AGPL-3.0-or-later"
|
||||
"testEnvironment": "jsdom"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -16,6 +16,9 @@
|
|||
height="1066.6667"
|
||||
viewBox="0 0 1066.6667 1066.6667"
|
||||
sodipodi:docname="llama-nightscape.svg"
|
||||
inkscape:export-filename="/home/konrad/www/vikunja/frontend/public/images/llama-nightscape.png"
|
||||
inkscape:export-xdpi="172.8"
|
||||
inkscape:export-ydpi="172.8"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
|
||||
id="metadata8"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
|
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Vikunja</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="Vikunja (/vɪˈkuːnjə/) - The to-do app to organize your life.">
|
||||
<meta name="hash" content="<%= webpack.hash %>"/>
|
||||
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/open-sans-v15-latin-700italic.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/open-sans-v15-latin-italic.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/quicksand-v7-latin-300.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/quicksand-v7-latin-500.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/quicksand-v7-latin-700.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/open-sans-v15-latin-regular.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/open-sans-v15-latin-700.woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/quicksand-v7-latin-regular.woff2" as="font">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but Vikunja doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script>
|
||||
//
|
||||
// This variable points the frontend to the api.
|
||||
// It has to be the full url, including the last /api/v1 part and port.
|
||||
// You can change this if your api is not reachable on the same port as the frontend.
|
||||
window.API_URL = 'http://localhost:3456/api/v1'
|
||||
//
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -2,8 +2,8 @@ const path = require('path')
|
|||
const express = require('express')
|
||||
const app = express()
|
||||
|
||||
const p = path.join(__dirname, '..', 'dist-dev')
|
||||
const port = 5000
|
||||
const p = path.join(__dirname, '..', 'dist')
|
||||
const port = 8080
|
||||
|
||||
app.use(express.static(p))
|
||||
// Handle urls set by the frontend
|
||||
|
|
52
src/App.vue
52
src/App.vue
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :class="{'is-touch': isTouch}">
|
||||
<div>
|
||||
<div :class="{'is-hidden': !online}">
|
||||
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
|
||||
<div class="offline" style="height: 0;width: 0;"></div>
|
||||
|
@ -23,18 +23,18 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState, mapGetters} from 'vuex'
|
||||
import isTouchDevice from 'is-touch-device'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import authTypes from './models/authTypes'
|
||||
|
||||
import Notification from './components/misc/notification'
|
||||
import {KEYBOARD_SHORTCUTS_ACTIVE, ONLINE} from './store/mutation-types'
|
||||
import KeyboardShortcuts from './components/misc/keyboard-shortcuts'
|
||||
import TopNavigation from './components/home/topNavigation'
|
||||
import ContentAuth from './components/home/contentAuth'
|
||||
import ContentLinkShare from './components/home/contentLinkShare'
|
||||
import ContentNoAuth from './components/home/contentNoAuth'
|
||||
import {setLanguage} from './i18n/setup'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import TopNavigation from '@/components/home/topNavigation'
|
||||
import ContentAuth from '@/components/home/contentAuth'
|
||||
import ContentLinkShare from '@/components/home/contentLinkShare'
|
||||
import ContentNoAuth from '@/components/home/contentNoAuth'
|
||||
import {setLanguage} from '@/i18n/setup'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
|
@ -50,13 +50,9 @@ export default {
|
|||
this.setupOnlineStatus()
|
||||
this.setupPasswortResetRedirect()
|
||||
this.setupEmailVerificationRedirect()
|
||||
this.setupAccountDeletionVerification()
|
||||
},
|
||||
beforeCreate() {
|
||||
this.$store.dispatch('config/update')
|
||||
.then(() => {
|
||||
this.$store.dispatch('auth/checkAuth')
|
||||
})
|
||||
this.$store.dispatch('auth/checkAuth')
|
||||
|
||||
setLanguage()
|
||||
|
@ -67,19 +63,12 @@ export default {
|
|||
this.$router.push({name: 'home'})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isTouch() {
|
||||
return isTouchDevice()
|
||||
},
|
||||
...mapState({
|
||||
online: ONLINE,
|
||||
keyboardShortcutsActive: KEYBOARD_SHORTCUTS_ACTIVE,
|
||||
}),
|
||||
...mapGetters('auth', [
|
||||
'authUser',
|
||||
'authLinkShare',
|
||||
]),
|
||||
},
|
||||
computed: mapState({
|
||||
authUser: state => state.auth.authenticated && (state.auth.info && state.auth.info.type === authTypes.USER),
|
||||
authLinkShare: state => state.auth.authenticated && (state.auth.info && state.auth.info.type === authTypes.LINK_SHARE),
|
||||
online: ONLINE,
|
||||
keyboardShortcutsActive: KEYBOARD_SHORTCUTS_ACTIVE,
|
||||
}),
|
||||
methods: {
|
||||
setupOnlineStatus() {
|
||||
this.$store.commit(ONLINE, navigator.onLine)
|
||||
|
@ -100,17 +89,6 @@ export default {
|
|||
this.$router.push({name: 'user.login'})
|
||||
}
|
||||
},
|
||||
setupAccountDeletionVerification() {
|
||||
if (typeof this.$route.query.accountDeletionConfirm !== 'undefined') {
|
||||
const accountDeletionService = new AccountDeleteService()
|
||||
accountDeletionService.confirm(this.$route.query.accountDeletionConfirm)
|
||||
.then(() => {
|
||||
this.success({message: this.$t('user.deletion.confirmSuccess')})
|
||||
this.$store.dispatch('auth/refreshUserInfo')
|
||||
})
|
||||
.catch(e => this.error(e))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"SW_UPDATED": "swUpdated"
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// Cache assets
|
||||
workbox.routing.registerRoute(
|
||||
// This regexp matches all files in precache-manifest
|
||||
new RegExp('.+\\.(css|json|js|svg|woff2|png|html|txt|wav)$'),
|
||||
new workbox.strategies.StaleWhileRevalidate(),
|
||||
)
|
||||
|
||||
// Always send api reqeusts through the network
|
||||
workbox.routing.registerRoute(
|
||||
new RegExp('api\\/v1\\/.*$'),
|
||||
new workbox.strategies.NetworkOnly(),
|
||||
)
|
||||
|
||||
// This code listens for the user's confirmation to update the app.
|
||||
self.addEventListener('message', (e) => {
|
||||
if (!e.data) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (e.data) {
|
||||
case 'skipWaiting':
|
||||
self.skipWaiting()
|
||||
break
|
||||
default:
|
||||
// NOOP
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
const getBearerToken = async () => {
|
||||
// we can't get a client that sent the current request, therefore we need
|
||||
// to ask any controlled page for auth token
|
||||
const allClients = await self.clients.matchAll()
|
||||
const client = allClients.filter(client => client.type === 'window')[0]
|
||||
|
||||
// if there is no page in scope, we can't get any token
|
||||
// and we indicate it with null value
|
||||
if (!client) {
|
||||
return null
|
||||
}
|
||||
|
||||
// to communicate with a page we will use MessageChannels
|
||||
// they expose pipe-like interface, where a receiver of
|
||||
// a message uses one end of a port for messaging and
|
||||
// we use the other end for listening
|
||||
const channel = new MessageChannel()
|
||||
|
||||
client.postMessage({
|
||||
'action': 'getBearerToken',
|
||||
}, [channel.port1])
|
||||
|
||||
// ports support only onmessage callback which
|
||||
// is cumbersome to use, so we wrap it with Promise
|
||||
return new Promise((resolve, reject) => {
|
||||
channel.port2.onmessage = event => {
|
||||
if (event.data.error) {
|
||||
console.error('Port error', event.error)
|
||||
reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data.authToken)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Notification action
|
||||
self.addEventListener('notificationclick', function (event) {
|
||||
const taskId = event.notification.data.taskId
|
||||
event.notification.close()
|
||||
|
||||
switch (event.action) {
|
||||
case 'mark-as-done':
|
||||
// FIXME: Ugly as hell, but no other way of doing this, since we can't use modules
|
||||
// in service workers for now.
|
||||
fetch('/config.json')
|
||||
.then(r => r.json())
|
||||
.then(config => {
|
||||
|
||||
getBearerToken()
|
||||
.then(token => {
|
||||
fetch(`${config.VIKUNJA_API_BASE_URL}tasks/${taskId}`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({id: taskId, done: true}),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(r => {
|
||||
console.debug('Task marked as done from notification', r)
|
||||
})
|
||||
.catch(e => {
|
||||
console.debug('Error marking task as done from notification', e)
|
||||
})
|
||||
})
|
||||
})
|
||||
break
|
||||
case 'show-task':
|
||||
clients.openWindow(`/tasks/${taskId}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
workbox.core.clientsClaim()
|
||||
// The precaching code provided by Workbox.
|
||||
self.__precacheManifest = [].concat(self.__precacheManifest || [])
|
||||
workbox.precaching.precacheAndRoute(self.__precacheManifest, {})
|
|
@ -5,7 +5,7 @@
|
|||
</a>
|
||||
<div
|
||||
:class="{'has-background': background}"
|
||||
:style="{'background-image': background && `url(${background})`}"
|
||||
:style="{'background-image': `url(${background})`}"
|
||||
class="app-container"
|
||||
>
|
||||
<navigation/>
|
||||
|
@ -44,8 +44,8 @@
|
|||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
|
||||
import Navigation from '@/components/home/navigation.vue'
|
||||
import QuickActions from '@/components/quick-actions/quick-actions.vue'
|
||||
import Navigation from '@/components/home/navigation'
|
||||
import QuickActions from '@/components/quick-actions/quick-actions'
|
||||
|
||||
export default {
|
||||
name: 'contentAuth',
|
||||
|
|
|
@ -13,8 +13,16 @@
|
|||
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
|
||||
</h1>
|
||||
<div class="box has-text-left view">
|
||||
<div class="logout">
|
||||
<x-button @click="logout()" type="secondary">
|
||||
<span>{{ $t('user.auth.logout') }}</span>
|
||||
<span class="icon is-small">
|
||||
<icon icon="sign-out-alt"/>
|
||||
</span>
|
||||
</x-button>
|
||||
</div>
|
||||
<router-view/>
|
||||
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank" rel="noreferrer noopener nofollow">
|
||||
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank">
|
||||
{{ $t('misc.poweredBy') }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="no-auth-wrapper">
|
||||
<div class="noauth-container">
|
||||
<img alt="Vikunja" src="/images/logo-full.svg" width="400" height="117"/>
|
||||
<img alt="Vikunja" src="/images/logo-full.svg"/>
|
||||
<div class="message is-info" v-if="motd !== ''">
|
||||
<div class="message-header">
|
||||
<p>{{ $t('misc.info') }}</p>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div :class="{'is-active': menuActive}" class="namespace-container">
|
||||
<div class="menu top-menu">
|
||||
<router-link :to="{name: 'home'}" class="logo">
|
||||
<img alt="Vikunja" src="/images/logo-full.svg" width="164" height="48"/>
|
||||
<img alt="Vikunja" src="/images/logo-full.svg"/>
|
||||
</router-link>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
|
@ -49,7 +49,7 @@
|
|||
</div>
|
||||
|
||||
<aside class="menu namespaces-lists loader-container" :class="{'is-loading': loading}">
|
||||
<template v-for="(n, nk) in namespaces">
|
||||
<template v-for="n in namespaces">
|
||||
<div :key="n.id" class="namespace-title" :class="{'has-menu': n.id > 0}">
|
||||
<span
|
||||
@click="toggleLists(n.id)"
|
||||
|
@ -73,47 +73,18 @@
|
|||
</a>
|
||||
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
|
||||
</div>
|
||||
<div
|
||||
:key="n.id + 'child'"
|
||||
class="more-container"
|
||||
v-if="typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true"
|
||||
>
|
||||
<!--
|
||||
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
|
||||
triggered by the change needs to have access to the current namespace
|
||||
-->
|
||||
<draggable
|
||||
:value="activeLists[nk]"
|
||||
@input="(lists) => updateActiveLists(n, lists)"
|
||||
:group="`namespace-${n.id}-lists`"
|
||||
@start="() => drag = true"
|
||||
@end="e => saveListPosition(e, nk)"
|
||||
v-bind="dragOptions"
|
||||
handle=".handle"
|
||||
:disabled="n.id < 0"
|
||||
:class="{'dragging-disabled': n.id < 0}"
|
||||
>
|
||||
<transition-group
|
||||
type="transition"
|
||||
:name="!drag ? 'flip-list' : null"
|
||||
tag="ul"
|
||||
class="menu-list can-be-hidden"
|
||||
>
|
||||
<li
|
||||
v-for="l in activeLists[nk]"
|
||||
:key="l.id"
|
||||
class="loader-container"
|
||||
:class="{'is-loading': listUpdating[l.id]}"
|
||||
>
|
||||
<div :key="n.id + 'child'" class="more-container" v-if="typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true">
|
||||
<ul class="menu-list can-be-hidden">
|
||||
<template v-for="l in n.lists">
|
||||
<!-- This is a bit ugly but vue wouldn't want to let me filter this - probably because the lists
|
||||
are nested inside of the namespaces makes it a lot harder.-->
|
||||
<li :key="l.id" v-if="!l.isArchived">
|
||||
<router-link
|
||||
class="list-menu-link"
|
||||
:class="{'router-link-exact-active': currentList.id === l.id}"
|
||||
:to="{ name: 'list.index', params: { listId: l.id} }"
|
||||
tag="span"
|
||||
>
|
||||
<span class="icon handle">
|
||||
<icon icon="grip-lines"/>
|
||||
</span>
|
||||
<span
|
||||
:style="{ backgroundColor: l.hexColor }"
|
||||
class="color-bubble"
|
||||
|
@ -133,12 +104,12 @@
|
|||
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
|
||||
<span class="list-setting-spacer" v-else></span>
|
||||
</li>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank" rel="noreferrer noopener nofollow">
|
||||
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank">
|
||||
{{ $t('misc.poweredBy') }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -147,41 +118,27 @@
|
|||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import {CURRENT_LIST, MENU_ACTIVE, LOADING, LOADING_MODULE} from '@/store/mutation-types'
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown'
|
||||
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||
|
||||
export default {
|
||||
name: 'navigation',
|
||||
data() {
|
||||
return {
|
||||
listsVisible: {},
|
||||
drag: false,
|
||||
dragOptions: {
|
||||
animation: 100,
|
||||
ghostClass: 'ghost',
|
||||
},
|
||||
listUpdating: {},
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ListSettingsDropdown,
|
||||
NamespaceSettingsDropdown,
|
||||
draggable,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
namespaces: state => state.namespaces.namespaces.filter(n => !n.isArchived),
|
||||
currentList: CURRENT_LIST,
|
||||
background: 'background',
|
||||
menuActive: MENU_ACTIVE,
|
||||
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
|
||||
}),
|
||||
activeLists() {
|
||||
return this.namespaces.map(({lists}) => lists.filter(item => !item.isArchived))
|
||||
},
|
||||
},
|
||||
computed: mapState({
|
||||
namespaces: state => state.namespaces.namespaces.filter(n => !n.isArchived),
|
||||
currentList: CURRENT_LIST,
|
||||
background: 'background',
|
||||
menuActive: MENU_ACTIVE,
|
||||
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
|
||||
}),
|
||||
beforeCreate() {
|
||||
this.$store.dispatch('namespaces/loadNamespaces')
|
||||
.then(namespaces => {
|
||||
|
@ -219,45 +176,6 @@ export default {
|
|||
toggleLists(namespaceId) {
|
||||
this.$set(this.listsVisible, namespaceId, !this.listsVisible[namespaceId] ?? false)
|
||||
},
|
||||
updateActiveLists(namespace, activeLists) {
|
||||
// this is a bit hacky: since we do have to filter out the archived items from the list
|
||||
// for vue draggable updating it is not as simple as replacing it.
|
||||
// instead we iterate over the non archived items in the old list and replace them with the ones in their new order
|
||||
const lists = namespace.lists.map((item) => {
|
||||
if (item.isArchived) {
|
||||
return item
|
||||
}
|
||||
return activeLists.shift()
|
||||
})
|
||||
|
||||
const newNamespace = {
|
||||
...namespace,
|
||||
lists,
|
||||
}
|
||||
|
||||
this.$store.commit('namespaces/setNamespaceById', newNamespace)
|
||||
},
|
||||
saveListPosition(e, namespaceIndex) {
|
||||
const listsActive = this.activeLists[namespaceIndex]
|
||||
const list = listsActive[e.newIndex]
|
||||
const listBefore = listsActive[e.newIndex - 1] ?? null
|
||||
const listAfter = listsActive[e.newIndex + 1] ?? null
|
||||
this.$set(this.listUpdating, list.id, true)
|
||||
|
||||
const position = calculateItemPosition(listBefore !== null ? listBefore.position : null, listAfter !== null ? listAfter.position : null)
|
||||
|
||||
// create a copy of the list in order to not violate vuex mutations
|
||||
this.$store.dispatch('lists/updateList', {
|
||||
...list,
|
||||
position,
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this.listUpdating, list.id, false)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
>
|
||||
<div class="navbar-brand">
|
||||
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||
<img width="164" height="48" alt="Vikunja" src="/images/logo-full-pride.svg" v-if="(new Date()).getMonth() === 5"/>
|
||||
<img width="164" height="48" alt="Vikunja" src="/images/logo-full.svg" v-else/>
|
||||
<img alt="Vikunja" src="/images/logo-full-pride.svg" v-if="(new Date()).getMonth() === 5"/>
|
||||
<img alt="Vikunja" src="/images/logo-full.svg" v-else/>
|
||||
</router-link>
|
||||
<a
|
||||
@click="$store.commit('toggleMenu')"
|
||||
|
@ -16,12 +16,14 @@
|
|||
@shortkey="() => $store.commit('toggleMenu')"
|
||||
v-shortkey="['ctrl', 'e']"
|
||||
>
|
||||
<icon icon="bars"></icon>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
@click="$store.commit('toggleMenu')"
|
||||
class="menu-show-button"
|
||||
>
|
||||
<icon icon="bars"></icon>
|
||||
</a>
|
||||
<div class="list-title" ref="listTitle" :style="{'display': currentList.id ? '': 'none'}">
|
||||
<template v-if="currentList.id">
|
||||
|
@ -47,7 +49,7 @@
|
|||
</a>
|
||||
<notifications/>
|
||||
<div class="user">
|
||||
<img :src="userAvatar" alt="" class="avatar" width="40" height="40"/>
|
||||
<img :src="userAvatar" alt="" class="avatar"/>
|
||||
<dropdown class="is-right" ref="usernameDropdown">
|
||||
<template v-slot:trigger>
|
||||
<x-button
|
||||
|
@ -67,7 +69,6 @@
|
|||
:href="imprintUrl"
|
||||
class="dropdown-item"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
v-if="imprintUrl">
|
||||
{{ $t('navigation.imprint') }}
|
||||
</a>
|
||||
|
@ -75,7 +76,6 @@
|
|||
:href="privacyPolicyUrl"
|
||||
class="dropdown-item"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
v-if="privacyPolicyUrl">
|
||||
{{ $t('navigation.privacy') }}
|
||||
</a>
|
||||
|
@ -97,11 +97,11 @@
|
|||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import {CURRENT_LIST, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
|
||||
import Rights from '@/models/constants/rights.json'
|
||||
import Update from '@/components/home/update.vue'
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import Notifications from '@/components/notifications/notifications.vue'
|
||||
import Rights from '@/models/rights.json'
|
||||
import Update from '@/components/home/update'
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown'
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import Notifications from '@/components/notifications/notifications'
|
||||
|
||||
export default {
|
||||
name: 'topNavigation',
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import swEvents from '@/ServiceWorker/events.json'
|
||||
|
||||
export default {
|
||||
name: 'update',
|
||||
data() {
|
||||
|
@ -18,7 +20,7 @@ export default {
|
|||
}
|
||||
},
|
||||
created() {
|
||||
document.addEventListener('swUpdated', this.showRefreshUI, {once: true})
|
||||
document.addEventListener(swEvents.SW_UPDATED, this.showRefreshUI, {once: true})
|
||||
|
||||
if (navigator && navigator.serviceWorker) {
|
||||
navigator.serviceWorker.addEventListener(
|
||||
|
|
|
@ -55,7 +55,7 @@ export default {
|
|||
computed: {
|
||||
showIconOnly() {
|
||||
return this.icon !== '' && typeof this.$slots.default === 'undefined'
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click(e) {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
<script>
|
||||
import verte from 'verte'
|
||||
import 'verte/dist/verte.css'
|
||||
|
||||
export default {
|
||||
name: 'colorPicker',
|
||||
|
@ -90,8 +91,6 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'verte/dist/verte.css';
|
||||
|
||||
.verte.is-empty {
|
||||
.verte__icon {
|
||||
opacity: 0;
|
||||
|
|
|
@ -137,18 +137,18 @@ export default {
|
|||
},
|
||||
props: {
|
||||
value: {
|
||||
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
|
||||
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string'
|
||||
},
|
||||
chooseDateLabel: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('input.datepicker.chooseDate')
|
||||
},
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setDateValue(this.value)
|
||||
|
|
|
@ -1,5 +1,24 @@
|
|||
<template>
|
||||
<div class="editor">
|
||||
<div :class="{'is-pulled-up': isEditEnabled}" class="editor">
|
||||
<div class="is-pulled-right mb-4" v-if="hasPreview && isEditEnabled && !hasEditBottom">
|
||||
<x-button
|
||||
v-if="!isEditActive"
|
||||
@click="toggleEdit"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
>
|
||||
<icon icon="pen"/>
|
||||
</x-button>
|
||||
<x-button
|
||||
v-else
|
||||
@click="toggleEdit"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
>
|
||||
{{ $t('input.editor.done') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
<vue-easymde
|
||||
|
@ -13,34 +32,24 @@
|
|||
<div class="preview content" v-html="preview" v-if="isPreviewActive && text !== ''">
|
||||
</div>
|
||||
|
||||
<p class="has-text-centered has-text-grey is-italic" v-if="showPreviewText">
|
||||
<p class="has-text-centered has-text-grey is-italic" v-if="isPreviewActive && text === '' && emptyText !== ''">
|
||||
{{ emptyText }}
|
||||
<template v-if="isEditEnabled">
|
||||
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>.
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<ul class="actions" v-if="bottomActions.length > 0">
|
||||
<template v-if="isEditEnabled && !showPreviewText && showSave">
|
||||
<ul class="actions">
|
||||
<template v-if="hasEditBottom && isEditEnabled">
|
||||
<li>
|
||||
<a v-if="!isEditActive" @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
|
||||
<a v-else @click="toggleEdit" class="done-edit">{{ $t('misc.save') }}</a>
|
||||
<a v-else @click="toggleEdit">{{ $t('input.editor.done') }}</a>
|
||||
</li>
|
||||
</template>
|
||||
<li v-for="(action, k) in bottomActions" :key="k">
|
||||
<a @click="action.action">{{ action.title }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<template v-else-if="showSave">
|
||||
<ul v-if="!isEditActive" class="actions">
|
||||
<li>
|
||||
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<x-button v-else @click="toggleEdit" type="secondary" :shadow="false">
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -49,7 +58,6 @@ import VueEasymde from 'vue-easymde'
|
|||
import EasyMDE from 'easymde'
|
||||
import marked from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
import hljs from 'highlight.js/lib/common'
|
||||
|
||||
import AttachmentModel from '../../models/attachment'
|
||||
import AttachmentService from '../../services/attachment'
|
||||
|
@ -88,6 +96,10 @@ export default {
|
|||
isEditEnabled: {
|
||||
default: true,
|
||||
},
|
||||
hasEditBottom: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
bottomActions: {
|
||||
default: () => [],
|
||||
},
|
||||
|
@ -95,15 +107,6 @@ export default {
|
|||
type: String,
|
||||
default: () => '',
|
||||
},
|
||||
showSave: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showPreviewText() {
|
||||
return this.isPreviewActive && this.text === '' && this.emptyText !== ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -114,7 +117,6 @@ export default {
|
|||
|
||||
preview: '',
|
||||
attachmentService: null,
|
||||
loadedAttachments: {},
|
||||
|
||||
config: {
|
||||
autoDownloadFontAwesome: false,
|
||||
|
@ -282,7 +284,7 @@ export default {
|
|||
// that in the end, only one change event is triggered to the outside per change.
|
||||
handleInput(val) {
|
||||
// Don't bubble if the text is up to date
|
||||
if (val === this.text) {
|
||||
if(val === this.text) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -363,16 +365,17 @@ export default {
|
|||
link: (href, title, text) => {
|
||||
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
|
||||
const html = linkRenderer.call(renderer, href, title, text)
|
||||
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
|
||||
return isLocal ? html : html.replace(/^<a /, `<a target="_blank" rel="noreferrer noopener nofollow" `)
|
||||
},
|
||||
},
|
||||
highlight: function (code, language) {
|
||||
const hljs = require('highlight.js')
|
||||
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
|
||||
return hljs.highlight(code, {language: validLanguage}).value
|
||||
},
|
||||
})
|
||||
|
||||
this.preview = DOMPurify.sanitize(marked(this.text), {ADD_ATTR: ['target']})
|
||||
this.preview = DOMPurify.sanitize(marked(this.text), { ADD_ATTR: ['target'] })
|
||||
|
||||
// Since the render function is synchronous, we can't do async http requests in it.
|
||||
// Therefore, we can't resolve the blob url at (markdown) compile time.
|
||||
|
@ -389,13 +392,6 @@ export default {
|
|||
const parts = img.dataset.src.substr(window.API_URL.length + 1).split('/')
|
||||
const taskId = parseInt(parts[1])
|
||||
const attachmentId = parseInt(parts[3])
|
||||
const cacheKey = `${taskId}-${attachmentId}`
|
||||
|
||||
if (typeof this.loadedAttachments[cacheKey] !== 'undefined') {
|
||||
img.src = this.loadedAttachments[cacheKey]
|
||||
continue
|
||||
}
|
||||
|
||||
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
|
||||
|
||||
if (this.attachmentService === null) {
|
||||
|
@ -405,7 +401,6 @@ export default {
|
|||
this.attachmentService.getBlobUrl(attachment)
|
||||
.then(url => {
|
||||
img.src = url
|
||||
this.loadedAttachments[cacheKey] = url
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -457,18 +452,15 @@ export default {
|
|||
<style lang="scss">
|
||||
@import '../../../node_modules/highlight.js/scss/base16/equilibrium-gray-light';
|
||||
@import '../../../node_modules/easymde/dist/easymde.min.css';
|
||||
@import '../../styles/theme/variables/all';
|
||||
|
||||
.editor {
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.preview.content {
|
||||
margin-bottom: .5rem;
|
||||
|
||||
ul li input[type="checkbox"] {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
.preview.content ul li input[type="checkbox"] {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -547,10 +539,6 @@ ul.actions {
|
|||
|
||||
&, a {
|
||||
color: $grey-500;
|
||||
|
||||
&.done-edit {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
:checked="checked"
|
||||
:disabled="disabled"
|
||||
:id="checkBoxId"
|
||||
@change="(event) => updateData(event.target.checked)"
|
||||
@change="updateData"
|
||||
style="display: none;"
|
||||
type="checkbox"/>
|
||||
<label :for="checkBoxId" class="check">
|
||||
|
@ -51,10 +51,10 @@ export default {
|
|||
this.checkBoxId = 'fancycheckbox' + Math.random()
|
||||
},
|
||||
methods: {
|
||||
updateData(checked) {
|
||||
this.checked = checked
|
||||
this.$emit('input', checked)
|
||||
this.$emit('change', checked)
|
||||
updateData(e) {
|
||||
this.checked = e.target.checked
|
||||
this.$emit('input', this.checked)
|
||||
this.$emit('change', e.target.checked)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -108,21 +108,21 @@ export default {
|
|||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
}
|
||||
},
|
||||
// The placeholder of the search input
|
||||
placeholder: {
|
||||
type: String,
|
||||
default() {
|
||||
return ''
|
||||
},
|
||||
}
|
||||
},
|
||||
// The search results where the @search listener needs to put the results into
|
||||
searchResults: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
}
|
||||
},
|
||||
// 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.
|
||||
|
@ -130,13 +130,13 @@ export default {
|
|||
type: String,
|
||||
default() {
|
||||
return ''
|
||||
},
|
||||
}
|
||||
},
|
||||
// The object with the value, updated every time an entry is selected.
|
||||
value: {
|
||||
default() {
|
||||
return null
|
||||
},
|
||||
}
|
||||
},
|
||||
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
|
||||
creatable: {
|
||||
|
@ -150,14 +150,14 @@ export default {
|
|||
type: String,
|
||||
default() {
|
||||
return this.$t('input.multiselect.createPlaceholder')
|
||||
},
|
||||
}
|
||||
},
|
||||
// The text shown next to an option.
|
||||
selectPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('input.multiselect.selectPlaceholder')
|
||||
},
|
||||
}
|
||||
},
|
||||
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
|
||||
multiple: {
|
||||
|
@ -222,7 +222,7 @@ export default {
|
|||
})
|
||||
},
|
||||
filteredSearchResults() {
|
||||
if (this.multiple && this.internalValue !== null && Array.isArray(this.internalValue)) {
|
||||
if (this.multiple && this.internalValue !== null) {
|
||||
return this.searchResults.filter(item => !this.internalValue.some(e => e === item))
|
||||
}
|
||||
|
||||
|
|
|
@ -75,9 +75,9 @@
|
|||
|
||||
<script>
|
||||
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import TaskSubscription from '@/components/misc/subscription.vue'
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import DropdownItem from '@/components/misc/dropdown-item'
|
||||
import TaskSubscription from '@/components/misc/subscription'
|
||||
|
||||
export default {
|
||||
name: 'list-settings-dropdown',
|
||||
|
@ -101,7 +101,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
backgroundsEnabled() {
|
||||
return this.$store.state.config.enabledBackgroundProviders !== null && this.$store.state.config.enabledBackgroundProviders.length > 0
|
||||
return this.$store.state.config.enabledBackgroundProviders.length > 0
|
||||
},
|
||||
listRoutePrefix() {
|
||||
let name = 'list'
|
||||
|
|
|
@ -131,8 +131,25 @@
|
|||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.labels') }}</label>
|
||||
<div class="control labels-list">
|
||||
<edit-labels v-model="labels" @change="changeLabelFilter"/>
|
||||
<div class="control">
|
||||
<multiselect
|
||||
:placeholder="$t('label.search')"
|
||||
@search="findLabels"
|
||||
:search-results="foundLabels"
|
||||
@select="label => addLabel(label)"
|
||||
label="title"
|
||||
:multiple="true"
|
||||
v-model="labels"
|
||||
>
|
||||
<template v-slot:tag="props">
|
||||
<span
|
||||
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
|
||||
class="tag ml-2 mt-2">
|
||||
<span>{{ props.item.title }}</span>
|
||||
<a @click="removeLabel(props.item)" class="delete is-small"></a>
|
||||
</span>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -181,48 +198,17 @@ import 'flatpickr/dist/flatpickr.css'
|
|||
import {formatISO} from 'date-fns'
|
||||
import differenceWith from 'lodash/differenceWith'
|
||||
|
||||
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
|
||||
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import PrioritySelect from '@/components/tasks/partials/prioritySelect'
|
||||
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
import UserService from '@/services/user'
|
||||
import ListService from '@/services/list'
|
||||
import NamespaceService from '@/services/namespace'
|
||||
import EditLabels from '@/components/tasks/partials/editLabels.vue'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskList.js
|
||||
const DEFAULT_PARAMS = {
|
||||
sort_by: [],
|
||||
order_by: [],
|
||||
filter_by: [],
|
||||
filter_value: [],
|
||||
filter_comparator: [],
|
||||
filter_include_nulls: true,
|
||||
filter_concat: 'or',
|
||||
s: '',
|
||||
}
|
||||
|
||||
const DEFAULT_FILTERS = {
|
||||
done: false,
|
||||
dueDate: '',
|
||||
requireAllFilters: false,
|
||||
priority: 0,
|
||||
usePriority: false,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
percentDone: 0,
|
||||
usePercentDone: false,
|
||||
reminders: '',
|
||||
assignees: '',
|
||||
labels: '',
|
||||
list_id: '',
|
||||
namespace: '',
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'filters',
|
||||
components: {
|
||||
EditLabels,
|
||||
PrioritySelect,
|
||||
Fancycheckbox,
|
||||
flatPickr,
|
||||
|
@ -231,8 +217,32 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
params: DEFAULT_PARAMS,
|
||||
filters: DEFAULT_FILTERS,
|
||||
params: {
|
||||
sort_by: [],
|
||||
order_by: [],
|
||||
filter_by: [],
|
||||
filter_value: [],
|
||||
filter_comparator: [],
|
||||
filter_include_nulls: true,
|
||||
filter_concat: 'or',
|
||||
s: '',
|
||||
},
|
||||
filters: {
|
||||
done: false,
|
||||
dueDate: '',
|
||||
requireAllFilters: false,
|
||||
priority: 0,
|
||||
usePriority: false,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
percentDone: 0,
|
||||
usePercentDone: false,
|
||||
reminders: '',
|
||||
assignees: '',
|
||||
labels: '',
|
||||
list_id: '',
|
||||
namespace: '',
|
||||
},
|
||||
|
||||
usersService: UserService,
|
||||
foundusers: [],
|
||||
|
@ -309,12 +319,9 @@ export default {
|
|||
this.prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
|
||||
this.prepareDate('reminders')
|
||||
this.prepareRelatedObjectFilter('users', 'assignees')
|
||||
this.prepareRelatedObjectFilter('labels', 'labels', 'label')
|
||||
this.prepareRelatedObjectFilter('lists', 'list_id')
|
||||
this.prepareRelatedObjectFilter('namespace')
|
||||
|
||||
this.prepareSingleValue('labels')
|
||||
const labelIds = (typeof this.filters.labels === 'string' ? this.filters.labels : '').split(',').map(i => parseInt(i))
|
||||
this.labels = (Object.values(this.$store.state.labels.labels).filter(l => labelIds.includes(l.id)) ?? [])
|
||||
},
|
||||
removePropertyFromFilter(propertyName) {
|
||||
// Because of the way arrays work, we can only ever remove one element at once.
|
||||
|
@ -328,11 +335,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
setDateFilter(filterName, variableName = null) {
|
||||
if (variableName === null) {
|
||||
variableName = filterName
|
||||
}
|
||||
|
||||
setDateFilter(filterName, variableName) {
|
||||
// Only filter if we have a start and end due date
|
||||
if (this.filters[variableName] !== '') {
|
||||
|
||||
|
|
|
@ -1,34 +1,16 @@
|
|||
<template>
|
||||
<div class="content">
|
||||
<h1>{{ $t('migrate.titleService', {name: name}) }}</h1>
|
||||
<h1>{{ $t('migrate.titleService', { name: name }) }}</h1>
|
||||
<p>{{ $t('migrate.descriptionDo') }}</p>
|
||||
<template v-if="isMigrating === false && message === '' && lastMigrationDate === null">
|
||||
<template v-if="isFileMigrator">
|
||||
<p>{{ $t('migrate.importUpload', {name: name}) }}</p>
|
||||
<input
|
||||
@change="migrate"
|
||||
class="is-hidden"
|
||||
ref="uploadInput"
|
||||
type="file"
|
||||
/>
|
||||
<x-button
|
||||
:loading="migrationService.loading"
|
||||
:disabled="migrationService.loading"
|
||||
@click="$refs.uploadInput.click()"
|
||||
>
|
||||
{{ $t('migrate.upload') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>{{ $t('migrate.authorize', {name: name}) }}</p>
|
||||
<x-button
|
||||
:loading="migrationService.loading"
|
||||
:disabled="migrationService.loading"
|
||||
:href="authUrl"
|
||||
>
|
||||
{{ $t('migrate.getStarted') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<p>{{ $t('migrate.authorize', {name: name}) }}</p>
|
||||
<x-button
|
||||
:loading="migrationService.loading"
|
||||
:disabled="migrationService.loading"
|
||||
:href="authUrl"
|
||||
>
|
||||
{{ $t('migrate.getStarted') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<div
|
||||
class="migration-in-progress-container"
|
||||
|
@ -36,7 +18,14 @@
|
|||
<div class="migration-in-progress">
|
||||
<img :alt="name" :src="`/images/migration/${identifier}.png`"/>
|
||||
<div class="progress-dots">
|
||||
<span v-for="i in progressDotsCount" :key="i" />
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<img alt="Vikunja" src="/images/logo.svg">
|
||||
</div>
|
||||
|
@ -44,7 +33,7 @@
|
|||
</div>
|
||||
<div v-else-if="lastMigrationDate">
|
||||
<p>
|
||||
{{ $t('migrate.alreadyMigrated1', {name: name, date: formatDate(lastMigrationDate)}) }}<br/>
|
||||
{{ $t('migrate.alreadyMigrated1', { name: name, date: formatDate(lastMigrationDate) }) }}<br/>
|
||||
{{ $t('migrate.alreadyMigrated2') }}
|
||||
</p>
|
||||
<div class="buttons">
|
||||
|
@ -64,22 +53,17 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import AbstractMigrationService from '../../services/migrator/abstractMigration'
|
||||
import AbstractMigrationFileService from '../../services/migrator/abstractMigrationFile'
|
||||
|
||||
const PROGRESS_DOTS_COUNT = 8
|
||||
import AbstractMigrationService from '../../services/migrator/abstractMigrationService'
|
||||
|
||||
export default {
|
||||
name: 'migration',
|
||||
data() {
|
||||
return {
|
||||
progressDotsCount: PROGRESS_DOTS_COUNT,
|
||||
authUrl: '',
|
||||
isMigrating: false,
|
||||
lastMigrationDate: null,
|
||||
message: '',
|
||||
migratorAuthCode: '',
|
||||
migrationService: null,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
@ -91,21 +75,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isFileMigrator: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.message = ''
|
||||
|
||||
if (this.isFileMigrator) {
|
||||
this.migrationService = new AbstractMigrationFileService(this.identifier)
|
||||
return
|
||||
}
|
||||
|
||||
this.migrationService = new AbstractMigrationService(this.identifier)
|
||||
this.getAuthUrl()
|
||||
this.message = ''
|
||||
|
||||
if (typeof this.$route.query.code !== 'undefined' || location.hash.startsWith('#token=')) {
|
||||
if (location.hash.startsWith('#token=')) {
|
||||
|
@ -148,11 +122,6 @@ export default {
|
|||
this.isMigrating = true
|
||||
this.lastMigrationDate = null
|
||||
this.message = ''
|
||||
|
||||
if (this.isFileMigrator) {
|
||||
return this.migrateFile()
|
||||
}
|
||||
|
||||
this.migrationService.migrate({code: this.migratorAuthCode})
|
||||
.then(r => {
|
||||
this.message = r.message
|
||||
|
@ -165,23 +134,6 @@ export default {
|
|||
this.isMigrating = false
|
||||
})
|
||||
},
|
||||
migrateFile() {
|
||||
if (this.$refs.uploadInput.files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.migrationService.migrate(this.$refs.uploadInput.files[0])
|
||||
.then(r => {
|
||||
this.message = r.message
|
||||
this.$store.dispatch('namespaces/loadNamespaces')
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isMigrating = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
<div class="api-url-info" v-else>
|
||||
<i18n path="apiConfig.signInOn">
|
||||
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
|
||||
<span class="url" v-tooltip="apiUrl"> {{ apiDomain() }} </span>
|
||||
</i18n>
|
||||
<br />
|
||||
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a>
|
||||
|
@ -46,24 +46,23 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
const API_DEFAULT_PORT = 3456
|
||||
|
||||
export default {
|
||||
name: 'apiConfig',
|
||||
data() {
|
||||
return {
|
||||
configureApi: false,
|
||||
apiUrl: window.API_URL,
|
||||
apiUrl: '',
|
||||
errorMsg: '',
|
||||
successMsg: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.apiUrl = window.API_URL
|
||||
if (this.apiUrl === '') {
|
||||
this.configureApi = true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
methods: {
|
||||
apiDomain() {
|
||||
if (window.API_URL.startsWith('/api/v1')) {
|
||||
return window.location.host
|
||||
|
@ -73,8 +72,6 @@ export default {
|
|||
.split(/[/?#]/)
|
||||
return urlParts[0]
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setApiUrl() {
|
||||
if (this.apiUrl === '') {
|
||||
return
|
||||
|
@ -134,17 +131,17 @@ export default {
|
|||
return Promise.reject(e)
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at port API_DEFAULT_PORT and https
|
||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||
// Check if it is reachable at port 3456 and https
|
||||
if (urlToCheck.port !== 3456) {
|
||||
urlToCheck.protocol = 'https:'
|
||||
urlToCheck.port = API_DEFAULT_PORT
|
||||
urlToCheck.port = 3456
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and https
|
||||
// Check if it is reachable at :3456 and /api/v1 and https
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
|
@ -157,17 +154,17 @@ export default {
|
|||
return Promise.reject(e)
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at port API_DEFAULT_PORT and http
|
||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||
// Check if it is reachable at port 3456 and http
|
||||
if (urlToCheck.port !== 3456) {
|
||||
urlToCheck.protocol = 'http:'
|
||||
urlToCheck.port = API_DEFAULT_PORT
|
||||
urlToCheck.port = 3456
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return this.$store.dispatch('config/update')
|
||||
}
|
||||
return Promise.reject(e)
|
||||
})
|
||||
.catch((e) => {
|
||||
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and http
|
||||
// Check if it is reachable at :3456 and /api/v1 and http
|
||||
urlToCheck.pathname = origUrlToCheck.pathname
|
||||
if (
|
||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||
|
@ -182,14 +179,14 @@ export default {
|
|||
.catch(() => {
|
||||
// Still not found, url is still invalid
|
||||
this.successMsg = ''
|
||||
this.errorMsg = this.$t('apiConfig.error', {domain: this.apiDomain})
|
||||
this.errorMsg = this.$t('apiConfig.error', {domain: this.apiDomain()})
|
||||
window.API_URL = oldUrl
|
||||
})
|
||||
.then((r) => {
|
||||
if (typeof r !== 'undefined') {
|
||||
// Set it + save it to local storage to save us the hoops
|
||||
this.errorMsg = ''
|
||||
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain})
|
||||
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain()})
|
||||
localStorage.setItem('API_URL', window.API_URL)
|
||||
this.configureApi = false
|
||||
this.apiUrl = window.API_URL
|
||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
|||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="notification is-danger">
|
||||
<i18n path="loadingError.failed">
|
||||
<a @click="() => location.reload()">{{ $t('loadingError.tryAgain') }}</a>
|
||||
<a href="https://vikunja.io/contact/" rel="noreferrer noopener nofollow" target="_blank">{{ $t('loadingError.contact') }}</a>
|
||||
<a href="https://vikunja.io/contact/">{{ $t('loadingError.contact') }}</a>
|
||||
</i18n>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
|
||||
<script>
|
||||
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
|
||||
import Shortcut from '@/components/misc/shortcut.vue'
|
||||
import Shortcut from '@/components/misc/shortcut'
|
||||
|
||||
export default {
|
||||
name: 'keyboard-shortcuts',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="legal-links">
|
||||
<a :href="imprintUrl" rel="noreferrer noopener nofollow" target="_blank" v-if="imprintUrl">{{ $t('navigation.imprint') }}</a>
|
||||
<a :href="imprintUrl" target="_blank" v-if="imprintUrl">{{ $t('navigation.imprint') }}</a>
|
||||
<span v-if="imprintUrl && privacyPolicyUrl"> | </span>
|
||||
<a :href="privacyPolicyUrl" rel="noreferrer noopener nofollow" target="_blank" v-if="privacyPolicyUrl">{{ $t('navigation.privacy') }}</a>
|
||||
<a :href="privacyPolicyUrl" target="_blank" v-if="privacyPolicyUrl">{{ $t('navigation.privacy') }}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
name: 'nothing',
|
||||
name: 'nothing'
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
|||
keys: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -57,7 +57,7 @@ export default {
|
|||
if (this.disabled) {
|
||||
return this.$t('task.subscription.subscribedThroughParent', {
|
||||
entity: this.entity,
|
||||
parent: this.subscription.entity,
|
||||
parent: this.subscription.entity
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,7 @@ export default {
|
|||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,43 +1,53 @@
|
|||
<template>
|
||||
<multiselect
|
||||
:loading="namespaceService.loading"
|
||||
:placeholder="$t('namespace.search')"
|
||||
@search="findNamespaces"
|
||||
:search-results="namespaces"
|
||||
@select="select"
|
||||
label="title"
|
||||
:search-delay="10"
|
||||
v-model="namespace"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import NamespaceService from '../../services/namespace'
|
||||
import NamespaceModel from '../../models/namespace'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'namespace-search',
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
namespaceService: NamespaceService,
|
||||
namespace: NamespaceModel,
|
||||
namespaces: [],
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Multiselect,
|
||||
},
|
||||
computed: {
|
||||
namespaces() {
|
||||
if (this.query === '') {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.$store.state.namespaces.namespaces.filter(n => {
|
||||
return !n.isArchived &&
|
||||
n.id > 0 &&
|
||||
n.title.toLowerCase().includes(this.query.toLowerCase())
|
||||
})
|
||||
},
|
||||
created() {
|
||||
this.namespaceService = new NamespaceService()
|
||||
},
|
||||
methods: {
|
||||
findNamespaces(query) {
|
||||
this.query = query
|
||||
if (query === '') {
|
||||
this.clearAll()
|
||||
return
|
||||
}
|
||||
|
||||
this.namespaceService.getAll({}, {s: query})
|
||||
.then(response => {
|
||||
this.$set(this, 'namespaces', response)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
},
|
||||
clearAll() {
|
||||
this.$set(this, 'namespaces', [])
|
||||
},
|
||||
select(namespace) {
|
||||
this.$emit('selected', namespace)
|
||||
|
|
|
@ -53,9 +53,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import TaskSubscription from '@/components/misc/subscription.vue'
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import DropdownItem from '@/components/misc/dropdown-item'
|
||||
import TaskSubscription from '@/components/misc/subscription'
|
||||
|
||||
export default {
|
||||
name: 'namespace-settings-dropdown',
|
||||
|
|
|
@ -49,8 +49,8 @@
|
|||
|
||||
<script>
|
||||
import NotificationService from '@/services/notification'
|
||||
import User from '@/components/misc/user.vue'
|
||||
import names from '@/models/constants/notificationNames.json'
|
||||
import User from '@/components/misc/user'
|
||||
import names from '@/models/notificationNames.json'
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
|
|
|
@ -62,8 +62,8 @@ import TeamModel from '@/models/team'
|
|||
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
|
||||
import ListModel from '@/models/list'
|
||||
import createTask from '@/components/tasks/mixins/createTask'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
import {getHistory} from '../../modules/listHistory'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic'
|
||||
import {getHistory} from '@/modules/listHistory'
|
||||
|
||||
const TYPE_LIST = 'list'
|
||||
const TYPE_TASK = 'task'
|
||||
|
@ -127,10 +127,6 @@ export default {
|
|||
...Object.values(this.$store.state.lists)])]
|
||||
|
||||
lists = (allLists.filter(l => {
|
||||
if (typeof l === 'undefined' || l === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (l.isArchived) {
|
||||
return false
|
||||
}
|
||||
|
@ -481,7 +477,7 @@ export default {
|
|||
reset() {
|
||||
this.query = ''
|
||||
this.selectedCmd = null
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -173,7 +173,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import rights from '../../models/constants/rights'
|
||||
import rights from '../../models/rights'
|
||||
|
||||
import LinkShareService from '../../services/linkShare'
|
||||
import LinkShareModel from '../../models/linkShare'
|
||||
|
|
|
@ -145,9 +145,9 @@ import TeamListService from '../../services/teamList'
|
|||
import TeamService from '../../services/team'
|
||||
import TeamModel from '../../models/team'
|
||||
|
||||
import rights from '../../models/constants/rights.json'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import Nothing from '@/components/misc/nothing.vue'
|
||||
import rights from '../../models/rights'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
import Nothing from '@/components/misc/nothing'
|
||||
|
||||
export default {
|
||||
name: 'userTeamShare',
|
||||
|
@ -235,11 +235,11 @@ export default {
|
|||
this.searchLabel = 'username'
|
||||
|
||||
if (this.type === 'list') {
|
||||
this.typeString = 'list'
|
||||
this.typeString = `list`
|
||||
this.stuffService = new UserListService()
|
||||
this.stuffModel = new UserListModel({listId: this.id})
|
||||
} else if (this.type === 'namespace') {
|
||||
this.typeString = 'namespace'
|
||||
this.typeString = `namespace`
|
||||
this.stuffService = new UserNamespaceService()
|
||||
this.stuffModel = new UserNamespaceModel({
|
||||
namespaceId: this.id,
|
||||
|
@ -253,11 +253,11 @@ export default {
|
|||
this.searchLabel = 'name'
|
||||
|
||||
if (this.type === 'list') {
|
||||
this.typeString = 'list'
|
||||
this.typeString = `list`
|
||||
this.stuffService = new TeamListService()
|
||||
this.stuffModel = new TeamListModel({listId: this.id})
|
||||
} else if (this.type === 'namespace') {
|
||||
this.typeString = 'namespace'
|
||||
this.typeString = `namespace`
|
||||
this.stuffService = new TeamNamespaceService()
|
||||
this.stuffModel = new TeamNamespaceModel({
|
||||
namespaceId: this.id,
|
||||
|
@ -278,7 +278,7 @@ export default {
|
|||
.then((r) => {
|
||||
this.$set(this, 'sharables', r)
|
||||
r.forEach((s) =>
|
||||
this.$set(this.selectedRight, s.id, s.right),
|
||||
this.$set(this.selectedRight, s.id, s.right)
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
<template>
|
||||
<div class="task-add">
|
||||
<div class="field is-grouped">
|
||||
<p class="control has-icons-left is-expanded">
|
||||
<input
|
||||
:disabled="taskService.loading"
|
||||
@keyup.enter="addTask()"
|
||||
class="input"
|
||||
:placeholder="$t('list.list.addPlaceholder')"
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="newTaskTitle"
|
||||
ref="newTaskInput"
|
||||
@keyup="errorMessage = ''"
|
||||
/>
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="tasks"/>
|
||||
</span>
|
||||
</p>
|
||||
<p class="control">
|
||||
<x-button
|
||||
:disabled="newTaskTitle === '' || taskService.loading"
|
||||
@click="addTask()"
|
||||
icon="plus"
|
||||
:loading="taskService.loading"
|
||||
>
|
||||
{{ $t('list.list.add') }}
|
||||
</x-button>
|
||||
</p>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errorMessage !== ''">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<quick-add-magic v-if="errorMessage === ''"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TaskService from '../../services/task'
|
||||
import createTask from '@/components/tasks/mixins/createTask'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
|
||||
export default {
|
||||
name: 'add-task',
|
||||
data() {
|
||||
return {
|
||||
newTaskTitle: '',
|
||||
taskService: TaskService,
|
||||
errorMessage: '',
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
createTask,
|
||||
],
|
||||
components: {
|
||||
QuickAddMagic,
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
},
|
||||
props: {
|
||||
defaultPosition: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addTask() {
|
||||
if (this.newTaskTitle === '') {
|
||||
this.errorMessage = this.$t('list.create.addTitleRequired')
|
||||
return
|
||||
}
|
||||
this.errorMessage = ''
|
||||
|
||||
if (this.taskService.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
this.createNewTask(this.newTaskTitle, 0, this.$store.state.auth.settings.defaultListId, this.defaultPosition)
|
||||
.then(task => {
|
||||
this.newTaskTitle = ''
|
||||
this.$emit('taskAdded', task)
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === 'NO_LIST') {
|
||||
this.errorMessage = this.$t('list.create.addListRequired')
|
||||
return
|
||||
}
|
||||
this.error(e)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.task-add {
|
||||
margin-bottom: 0;
|
||||
|
||||
.button {
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -72,7 +72,7 @@
|
|||
import ListService from '../../services/list'
|
||||
import TaskService from '../../services/task'
|
||||
import TaskModel from '../../models/task'
|
||||
import priorities from '../../models/constants/priorities'
|
||||
import priorities from '../../models/priorities'
|
||||
import EditLabels from './partials/editLabels'
|
||||
import Reminders from './partials/reminders'
|
||||
import ColorPicker from '../input/colorPicker'
|
||||
|
@ -100,7 +100,7 @@ export default {
|
|||
Reminders,
|
||||
EditLabels,
|
||||
editor: () => ({
|
||||
component: import('../../components/input/editor'),
|
||||
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
</x-button>
|
||||
</div>
|
||||
<filter-popup
|
||||
@change="loadTasks()"
|
||||
@change="loadTasks"
|
||||
:visible="showTaskFilter"
|
||||
v-model="params"
|
||||
/>
|
||||
|
@ -24,10 +24,18 @@
|
|||
class="month"
|
||||
v-for="(m, mk) in days[yk]"
|
||||
>
|
||||
{{ formatYear(new Date(`${yk}-${parseInt(mk) + 1}-01`)) }}
|
||||
{{
|
||||
new Date(
|
||||
new Date(yk).setMonth(mk)
|
||||
).toLocaleString('en-us', {month: 'long'})
|
||||
}},
|
||||
{{ new Date(yk).getFullYear() }}
|
||||
<div class="days">
|
||||
<div
|
||||
:class="{ today: d.toDateString() === now.toDateString() }"
|
||||
:class="{
|
||||
today:
|
||||
d.toDateString() === now.toDateString(),
|
||||
}"
|
||||
:key="dk + 'day'"
|
||||
:style="{ width: dayWidth + 'px' }"
|
||||
class="day"
|
||||
|
@ -188,13 +196,12 @@ import EditTask from './edit-task'
|
|||
|
||||
import TaskService from '../../services/task'
|
||||
import TaskModel from '../../models/task'
|
||||
import priorities from '../../models/constants/priorities'
|
||||
import priorities from '../../models/priorities'
|
||||
import PriorityLabel from './partials/priorityLabel'
|
||||
import TaskCollectionService from '../../services/taskCollection'
|
||||
import {mapState} from 'vuex'
|
||||
import Rights from '../../models/constants/rights.json'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||
import {format} from 'date-fns'
|
||||
import Rights from '../../models/rights.json'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup'
|
||||
|
||||
export default {
|
||||
name: 'GanttChart',
|
||||
|
@ -281,12 +288,12 @@ export default {
|
|||
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
|
||||
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]]
|
||||
let years = {}
|
||||
for (
|
||||
|
@ -298,13 +305,15 @@ export default {
|
|||
if (years[date.getFullYear() + ''] === undefined) {
|
||||
years[date.getFullYear() + ''] = {}
|
||||
}
|
||||
if (years[date.getFullYear() + ''][date.getMonth() + ''] === undefined) {
|
||||
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.$set(this, 'days', years)
|
||||
},
|
||||
parseTasks() {
|
||||
|
@ -379,7 +388,7 @@ export default {
|
|||
|
||||
let startDate = new Date(this.startDate)
|
||||
startDate.setDate(
|
||||
startDate.getDate() + newRect.left / this.dayWidth,
|
||||
startDate.getDate() + newRect.left / this.dayWidth
|
||||
)
|
||||
startDate.setUTCHours(0)
|
||||
startDate.setUTCMinutes(0)
|
||||
|
@ -388,7 +397,7 @@ export default {
|
|||
this.taskDragged.startDate = startDate
|
||||
let endDate = new Date(startDate)
|
||||
endDate.setDate(
|
||||
startDate.getDate() + newRect.width / this.dayWidth,
|
||||
startDate.getDate() + newRect.width / this.dayWidth
|
||||
)
|
||||
this.taskDragged.startDate = startDate
|
||||
this.taskDragged.endDate = endDate
|
||||
|
@ -431,7 +440,7 @@ export default {
|
|||
this.$set(
|
||||
this.theTasks,
|
||||
tt,
|
||||
this.addGantAttributes(r),
|
||||
this.addGantAttributes(r)
|
||||
)
|
||||
break
|
||||
}
|
||||
|
@ -479,9 +488,6 @@ export default {
|
|||
this.error(e)
|
||||
})
|
||||
},
|
||||
formatYear(date) {
|
||||
return format(date, 'MMMM, yyyy')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {parseTaskText} from '@/modules/parseTaskText'
|
||||
import {parseTaskText} from '@/helpers/parseTaskText'
|
||||
import TaskModel from '@/models/task'
|
||||
import {formatISO} from 'date-fns'
|
||||
import LabelTask from '@/models/labelTask'
|
||||
|
@ -6,12 +6,10 @@ import LabelModel from '@/models/label'
|
|||
import LabelTaskService from '@/services/labelTask'
|
||||
import {mapState} from 'vuex'
|
||||
import UserService from '@/services/user'
|
||||
import TaskService from '@/services/task'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
taskService: TaskService,
|
||||
labelTaskService: LabelTaskService,
|
||||
userService: UserService,
|
||||
}
|
||||
|
@ -19,35 +17,22 @@ export default {
|
|||
created() {
|
||||
this.labelTaskService = new LabelTaskService()
|
||||
this.userService = new UserService()
|
||||
this.taskService = new TaskService()
|
||||
},
|
||||
computed: mapState({
|
||||
labels: state => state.labels.labels,
|
||||
}),
|
||||
methods: {
|
||||
createNewTask(newTaskTitle, bucketId = 0, lId = 0, position = 0) {
|
||||
createNewTask(newTaskTitle, bucketId = 0, lId = 0) {
|
||||
const parsedTask = parseTaskText(newTaskTitle)
|
||||
const assignees = []
|
||||
|
||||
// Uses the following ways to get the list id of the new task:
|
||||
// 1. If specified in quick add magic, look in store if it exists and use it if it does
|
||||
// 2. Else check if a list was passed as parameter
|
||||
// 3. Otherwise use the id from the route parameter
|
||||
// 4. If none of the above worked, reject the promise with an error.
|
||||
let listId = null
|
||||
if (parsedTask.list !== null) {
|
||||
const list = this.$store.getters['lists/findListByExactname'](parsedTask.list)
|
||||
listId = list === null ? null : list.id
|
||||
}
|
||||
if (lId !== 0) {
|
||||
listId = lId
|
||||
}
|
||||
if (typeof this.$route.params.listId !== 'undefined') {
|
||||
listId = parseInt(this.$route.params.listId)
|
||||
}
|
||||
|
||||
if (typeof listId === 'undefined' || listId === null) {
|
||||
return Promise.reject('NO_LIST')
|
||||
if (listId === null) {
|
||||
listId = lId !== 0 ? lId : this.$route.params.listId
|
||||
}
|
||||
|
||||
// Separate closure because we need to wait for the results of the user search if users were entered in the
|
||||
|
@ -60,7 +45,6 @@ export default {
|
|||
priority: parsedTask.priority,
|
||||
assignees: assignees,
|
||||
bucketId: bucketId,
|
||||
position: position,
|
||||
})
|
||||
return this.taskService.create(task)
|
||||
.then(task => {
|
||||
|
@ -99,7 +83,7 @@ export default {
|
|||
.then(res => {
|
||||
return addLabelToTask(res)
|
||||
})
|
||||
.catch(e => Promise.reject(e)),
|
||||
.catch(e => Promise.reject(e))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -126,7 +110,7 @@ export default {
|
|||
assignees.push(user)
|
||||
}
|
||||
return Promise.resolve(users)
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,60 +1,5 @@
|
|||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||
const DEFAULT_PARAMS = {
|
||||
sort_by: ['position', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
}
|
||||
|
||||
function createPagination(totalPages, currentPage) {
|
||||
const pages = []
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
|
||||
// Show ellipsis instead of all pages
|
||||
if (
|
||||
i > 0 && // Always at least the first page
|
||||
(i + 1) < totalPages && // And the last page
|
||||
(
|
||||
// And the current with current + 1 and current - 1
|
||||
(i + 1) > currentPage + 1 ||
|
||||
(i + 1) < currentPage - 1
|
||||
)
|
||||
) {
|
||||
// Only add an ellipsis if the last page isn't already one
|
||||
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
|
||||
pages.push({
|
||||
number: 0,
|
||||
isEllipsis: true,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pages.push({
|
||||
number: i + 1,
|
||||
isEllipsis: false,
|
||||
})
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
export function getRouteForPagination(page = 1, type = 'list') {
|
||||
return {
|
||||
name: 'list.' + type,
|
||||
params: {
|
||||
type: type,
|
||||
},
|
||||
query: {
|
||||
page: page,
|
||||
},
|
||||
}
|
||||
}
|
||||
import TaskCollectionService from '../../../services/taskCollection'
|
||||
import {cloneDeep} from 'lodash'
|
||||
|
||||
/**
|
||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||
|
@ -62,9 +7,10 @@ export function getRouteForPagination(page = 1, type = 'list') {
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
taskCollectionService: new TaskCollectionService(),
|
||||
taskCollectionService: TaskCollectionService,
|
||||
tasks: [],
|
||||
|
||||
pages: [],
|
||||
currentPage: 0,
|
||||
|
||||
loadedList: null,
|
||||
|
@ -73,36 +19,39 @@ export default {
|
|||
searchTerm: '',
|
||||
|
||||
showTaskFilter: false,
|
||||
params: DEFAULT_PARAMS,
|
||||
params: {
|
||||
sort_by: ['done', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// Only listen for query path changes
|
||||
'$route.query': {
|
||||
handler: 'loadTasksForPage',
|
||||
immediate: true,
|
||||
},
|
||||
'$route.path': 'loadTasksOnSavedFilter',
|
||||
'$route.query': 'loadTasksForPage', // Only listen for query path changes
|
||||
},
|
||||
computed: {
|
||||
pages() {
|
||||
return createPagination(this.taskCollectionService.totalPages, this.currentPage)
|
||||
},
|
||||
beforeMount() {
|
||||
// Triggering loading the tasks in beforeMount lets the component maintain the current page, therefore the page
|
||||
// is not lost after navigating back from a task detail page for example.
|
||||
this.loadTasksForPage(this.$route.query)
|
||||
},
|
||||
created() {
|
||||
this.taskCollectionService = new TaskCollectionService()
|
||||
},
|
||||
methods: {
|
||||
loadTasks(
|
||||
page,
|
||||
search = '',
|
||||
params = null,
|
||||
forceLoading = false,
|
||||
) {
|
||||
|
||||
// Because this function is triggered every time on topNavigation, we're putting a condition here to only load it when we actually want to show tasks
|
||||
// FIXME: This is a bit hacky -> Cleanup.
|
||||
if (
|
||||
this.$route.name !== 'list.list' &&
|
||||
this.$route.name !== 'list.table' &&
|
||||
!forceLoading
|
||||
this.$route.name !== 'list.table'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
@ -123,24 +72,52 @@ export default {
|
|||
search: search,
|
||||
page: page,
|
||||
}
|
||||
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList) && !forceLoading) {
|
||||
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.tasks = []
|
||||
this.$set(this, 'tasks', [])
|
||||
|
||||
this.taskCollectionService.getAll(list, params, page)
|
||||
.then(r => {
|
||||
this.tasks = r
|
||||
this.$set(this, 'tasks', r)
|
||||
this.$set(this, 'pages', [])
|
||||
this.currentPage = page
|
||||
|
||||
for (let i = 0; i < this.taskCollectionService.totalPages; i++) {
|
||||
|
||||
// Show ellipsis instead of all pages
|
||||
if (
|
||||
i > 0 && // Always at least the first page
|
||||
(i + 1) < this.taskCollectionService.totalPages && // And the last page
|
||||
(
|
||||
// And the current with current + 1 and current - 1
|
||||
(i + 1) > this.currentPage + 1 ||
|
||||
(i + 1) < this.currentPage - 1
|
||||
)
|
||||
) {
|
||||
// Only add an ellipsis if the last page isn't already one
|
||||
if (this.pages[i - 1] && !this.pages[i - 1].isEllipsis) {
|
||||
this.pages.push({
|
||||
number: 0,
|
||||
isEllipsis: true,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.pages.push({
|
||||
number: i + 1,
|
||||
isEllipsis: false,
|
||||
})
|
||||
}
|
||||
|
||||
this.loadedList = cloneDeep(currentList)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
},
|
||||
|
||||
loadTasksForPage(e) {
|
||||
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
|
||||
let page = Number(e.page)
|
||||
|
@ -153,11 +130,6 @@ export default {
|
|||
}
|
||||
this.initTasks(page, search)
|
||||
},
|
||||
loadTasksOnSavedFilter() {
|
||||
if(typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
|
||||
this.loadTasks(1, '', null, true)
|
||||
}
|
||||
},
|
||||
sortTasks() {
|
||||
if (this.tasks === null || this.tasks === []) {
|
||||
return
|
||||
|
@ -168,9 +140,9 @@ export default {
|
|||
if (a.done > b.done)
|
||||
return 1
|
||||
|
||||
if (a.position < b.position)
|
||||
if (a.id > b.id)
|
||||
return -1
|
||||
if (a.position > b.position)
|
||||
if (a.id < b.id)
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
|
@ -196,23 +168,16 @@ export default {
|
|||
this.showTaskSearch = false
|
||||
}, 200)
|
||||
},
|
||||
saveTaskPosition(e) {
|
||||
this.drag = false
|
||||
|
||||
const task = this.tasks[e.newIndex]
|
||||
const taskBefore = this.tasks[e.newIndex - 1] ?? null
|
||||
const taskAfter = this.tasks[e.newIndex + 1] ?? null
|
||||
|
||||
task.position = calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null)
|
||||
|
||||
this.$store.dispatch('tasks/update', task)
|
||||
.then(r => {
|
||||
this.$set(this.tasks, e.newIndex, r)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
getRouteForPagination(page = 1, type = 'list') {
|
||||
return {
|
||||
name: 'list.' + type,
|
||||
params: {
|
||||
type: type,
|
||||
},
|
||||
query: {
|
||||
page: page,
|
||||
},
|
||||
}
|
||||
},
|
||||
getRouteForPagination,
|
||||
},
|
||||
}
|
|
@ -57,7 +57,7 @@
|
|||
@click.prevent.stop="downloadAttachment(a)"
|
||||
v-tooltip="$t('task.attachment.downloadTooltip')"
|
||||
>
|
||||
{{ $t('misc.download') }}
|
||||
{{ $t('task.attachment.download') }}
|
||||
</a>
|
||||
<a
|
||||
@click.stop="copyUrl(a)"
|
||||
|
@ -229,7 +229,7 @@ export default {
|
|||
.then((r) => {
|
||||
this.$store.commit(
|
||||
'attachments/removeById',
|
||||
this.attachmentToDelete.id,
|
||||
this.attachmentToDelete.id
|
||||
)
|
||||
this.success(r)
|
||||
})
|
||||
|
|
|
@ -77,8 +77,8 @@
|
|||
}
|
||||
"
|
||||
v-model="c.comment"
|
||||
:has-edit-bottom="true"
|
||||
:bottom-actions="actions[c.id]"
|
||||
:show-save="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -159,7 +159,7 @@ export default {
|
|||
name: 'comments',
|
||||
components: {
|
||||
editor: () => ({
|
||||
component: import('../../input/editor'),
|
||||
component: import(/* webpackChunkName: "editor" */ '../../input/editor'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
|
@ -208,9 +208,6 @@ export default {
|
|||
watch: {
|
||||
taskId() {
|
||||
this.loadComments()
|
||||
this.newComment.taskId = this.taskId
|
||||
this.commentEdit.taskId = this.taskId
|
||||
this.commentToDelete.taskId = this.taskId
|
||||
},
|
||||
canWrite() {
|
||||
this.makeActions()
|
||||
|
@ -253,7 +250,6 @@ export default {
|
|||
this.comments.push(r)
|
||||
this.newComment.comment = ''
|
||||
this.success({message: this.$t('task.comment.addedSuccess')})
|
||||
this.makeActions()
|
||||
})
|
||||
.catch((e) => {
|
||||
this.error(e)
|
||||
|
|
|
@ -23,15 +23,13 @@
|
|||
@change="save"
|
||||
:placeholder="$t('task.description.placeholder')"
|
||||
:empty-text="$t('task.description.empty')"
|
||||
:show-save="true"
|
||||
v-model="task.description"
|
||||
/>
|
||||
v-model="task.description"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingComponent from '@/components/misc/loading.vue'
|
||||
import ErrorComponent from '@/components/misc/error.vue'
|
||||
import LoadingComponent from '@/components/misc/loading'
|
||||
import ErrorComponent from '@/components/misc/error'
|
||||
|
||||
import {LOADING} from '@/store/mutation-types'
|
||||
import {mapState} from 'vuex'
|
||||
|
@ -40,7 +38,7 @@ export default {
|
|||
name: 'description',
|
||||
components: {
|
||||
editor: () => ({
|
||||
component: import('@/components/input/editor.vue'),
|
||||
component: import(/* webpackChunkName: "editor" */ '@/components/input/editor'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
|
@ -80,9 +78,8 @@ export default {
|
|||
this.saving = true
|
||||
|
||||
this.$store.dispatch('tasks/update', this.task)
|
||||
.then(t => {
|
||||
this.task = t
|
||||
this.$emit('input', t)
|
||||
.then(() => {
|
||||
this.$emit('input', this.task)
|
||||
this.saved = true
|
||||
setTimeout(() => {
|
||||
this.saved = false
|
||||
|
@ -94,7 +91,7 @@ export default {
|
|||
.finally(() => {
|
||||
this.saving = false
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -35,7 +35,7 @@ import UserModel from '../../../models/user'
|
|||
import ListUserService from '../../../services/listUsers'
|
||||
import TaskAssigneeService from '../../../services/taskAssignee'
|
||||
import User from '../../misc/user'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'editAssignees',
|
||||
|
|
|
@ -43,7 +43,7 @@ import differenceWith from 'lodash/differenceWith'
|
|||
import LabelModel from '../../../models/label'
|
||||
import LabelTaskService from '../../../services/labelTask'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
|
||||
|
||||
export default {
|
||||
|
@ -55,8 +55,7 @@ export default {
|
|||
},
|
||||
taskId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: () => 0,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
|
@ -101,19 +100,9 @@ export default {
|
|||
this.query = query
|
||||
},
|
||||
addLabel(label, showNotification = true) {
|
||||
const bubble = () => {
|
||||
this.$emit('input', this.labels)
|
||||
this.$emit('change', this.labels)
|
||||
}
|
||||
|
||||
if (this.taskId === 0) {
|
||||
bubble()
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId})
|
||||
.then(() => {
|
||||
bubble()
|
||||
this.$emit('input', this.labels)
|
||||
if (showNotification) {
|
||||
this.success({message: this.$t('task.label.addSuccess')})
|
||||
}
|
||||
|
@ -123,24 +112,15 @@ export default {
|
|||
})
|
||||
},
|
||||
removeLabel(label) {
|
||||
const removeFromState = () => {
|
||||
for (const l in this.labels) {
|
||||
if (this.labels[l].id === label.id) {
|
||||
this.labels.splice(l, 1)
|
||||
}
|
||||
}
|
||||
this.$emit('input', this.labels)
|
||||
this.$emit('change', this.labels)
|
||||
}
|
||||
|
||||
if (this.taskId === 0) {
|
||||
removeFromState()
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.dispatch('tasks/removeLabel', {label: label, taskId: this.taskId})
|
||||
.then(() => {
|
||||
removeFromState()
|
||||
// Remove the label from the list
|
||||
for (const l in this.labels) {
|
||||
if (this.labels[l].id === label.id) {
|
||||
this.labels.splice(l, 1)
|
||||
}
|
||||
}
|
||||
this.$emit('input', this.labels)
|
||||
this.success({message: this.$t('task.label.removeSuccess')})
|
||||
})
|
||||
.catch(e => {
|
||||
|
@ -148,10 +128,6 @@ export default {
|
|||
})
|
||||
},
|
||||
createAndAddLabel(title) {
|
||||
if (this.taskId === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const newLabel = new LabelModel({title: title})
|
||||
this.$store.dispatch('labels/createLabel', newLabel)
|
||||
.then(r => {
|
||||
|
|
|
@ -7,17 +7,16 @@
|
|||
<h1
|
||||
class="title input"
|
||||
:class="{'disabled': !canWrite}"
|
||||
@blur="save($event.target.textContent)"
|
||||
@keydown.enter.prevent.stop="$event.target.blur()"
|
||||
@focusout="save()"
|
||||
@keydown.enter.prevent.stop="save()"
|
||||
:contenteditable="canWrite ? 'true' : 'false'"
|
||||
spellcheck="false"
|
||||
ref="taskTitle">{{ task.title.trim() }}</h1>
|
||||
<transition name="fade">
|
||||
<span class="is-inline-flex is-align-items-center" v-if="loading && saving">
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
{{ $t('misc.saving') }}
|
||||
</span>
|
||||
<span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && showSavedMessage">
|
||||
<span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && saved">
|
||||
<icon icon="check" class="mr-2"/>
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
|
@ -26,22 +25,22 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import {LOADING} from '@/store/mutation-types'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'heading',
|
||||
data() {
|
||||
return {
|
||||
showSavedMessage: false,
|
||||
task: {title: '', identifier: '', index:''},
|
||||
taskTitle: '',
|
||||
saved: false,
|
||||
saving: false, // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['loading']),
|
||||
task() {
|
||||
return this.value
|
||||
},
|
||||
},
|
||||
computed: mapState({
|
||||
loading: LOADING,
|
||||
}),
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
|
@ -51,29 +50,43 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.task = newVal
|
||||
this.taskTitle = this.task.title
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.task = this.value
|
||||
this.taskTitle = this.task.title
|
||||
},
|
||||
methods: {
|
||||
save(title) {
|
||||
// We only want to save if the title was actually changed.
|
||||
// Because the contenteditable does not have a change event
|
||||
// we're building it ourselves and only continue
|
||||
// if the task title changed.
|
||||
if (title === this.task.title) {
|
||||
return
|
||||
}
|
||||
save() {
|
||||
this.$refs.taskTitle.spellcheck = false
|
||||
|
||||
// Pull the task title from the contenteditable
|
||||
let taskTitle = this.$refs.taskTitle.textContent
|
||||
this.task.title = taskTitle
|
||||
|
||||
// We only want to save if the title was actually change.
|
||||
// Because the contenteditable does not have a change event,
|
||||
// we're building it ourselves and only calling saveTask()
|
||||
// if the task title changed.
|
||||
if (this.task.title !== this.taskTitle) {
|
||||
this.$refs.taskTitle.blur()
|
||||
this.saveTask()
|
||||
this.taskTitle = taskTitle
|
||||
}
|
||||
},
|
||||
saveTask() {
|
||||
this.saving = true
|
||||
|
||||
const newTask = {
|
||||
...this.task,
|
||||
title,
|
||||
}
|
||||
|
||||
this.$store.dispatch('tasks/update', newTask)
|
||||
.then((task) => {
|
||||
this.$emit('input', task)
|
||||
this.showSavedMessage = true
|
||||
this.$store.dispatch('tasks/update', this.task)
|
||||
.then(() => {
|
||||
this.$emit('input', this.task)
|
||||
this.saved = true
|
||||
setTimeout(() => {
|
||||
this.showSavedMessage = false
|
||||
this.saved = false
|
||||
}, 2000)
|
||||
})
|
||||
.catch(e => {
|
||||
|
@ -82,7 +95,7 @@ export default {
|
|||
.finally(() => {
|
||||
this.saving = false
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'is-loading': loadingInternal || loading,
|
||||
'draggable': !(loadingInternal || loading),
|
||||
'has-light-text': !colorIsDark(task.hexColor) && task.hexColor !== `#${task.defaultColor}` && task.hexColor !== task.defaultColor,
|
||||
}"
|
||||
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
|
||||
@click.ctrl="() => markTaskAsDone(task)"
|
||||
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
|
||||
@click.meta="() => markTaskAsDone(task)"
|
||||
class="task loader-container draggable"
|
||||
>
|
||||
<span class="task-id">
|
||||
<span class="is-done" v-if="task.done">Done</span>
|
||||
<template v-if="task.identifier === ''">
|
||||
#{{ task.index }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ task.identifier }}
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
class="due-date"
|
||||
v-if="task.dueDate > 0"
|
||||
v-tooltip="formatDate(task.dueDate)">
|
||||
<span class="icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
</span>
|
||||
<span>
|
||||
{{ formatDateSince(task.dueDate) }}
|
||||
</span>
|
||||
</span>
|
||||
<h3>{{ task.title }}</h3>
|
||||
<progress
|
||||
class="progress is-small"
|
||||
v-if="task.percentDone > 0"
|
||||
:value="task.percentDone * 100" max="100">
|
||||
{{ task.percentDone * 100 }}%
|
||||
</progress>
|
||||
<div class="footer">
|
||||
<labels :labels="task.labels"/>
|
||||
<priority-label :priority="task.priority"/>
|
||||
<div class="assignees" v-if="task.assignees.length > 0">
|
||||
<user
|
||||
:avatar-size="24"
|
||||
:key="task.id + 'assignee' + u.id"
|
||||
:show-username="false"
|
||||
:user="u"
|
||||
v-for="u in task.assignees"
|
||||
/>
|
||||
</div>
|
||||
<span class="icon" v-if="task.attachments.length > 0">
|
||||
<icon icon="paperclip"/>
|
||||
</span>
|
||||
<span v-if="task.description" class="icon">
|
||||
<icon icon="align-left"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {playPop} from '../../../helpers/playPop'
|
||||
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
|
||||
import User from '../../../components/misc/user'
|
||||
import Labels from '../../../components/tasks/partials/labels'
|
||||
|
||||
export default {
|
||||
name: 'kanban-card',
|
||||
components: {
|
||||
PriorityLabel,
|
||||
User,
|
||||
Labels,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loadingInternal: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
task: {
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
markTaskAsDone(task) {
|
||||
this.loadingInternal = true
|
||||
this.$store.dispatch('tasks/update', {
|
||||
...task,
|
||||
done: !task.done,
|
||||
})
|
||||
.then(() => {
|
||||
if (task.done) {
|
||||
playPop()
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingInternal = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<multiselect
|
||||
class="control is-expanded"
|
||||
v-focus
|
||||
:loading="listSerivce.loading"
|
||||
:placeholder="$t('list.search')"
|
||||
@search="findLists"
|
||||
|
@ -11,7 +12,7 @@
|
|||
:select-placeholder="$t('list.searchSelect')"
|
||||
>
|
||||
<template v-slot:searchResult="props">
|
||||
<span class="list-namespace-title search-result">{{ namespace(props.option.namespaceId) }} ></span>
|
||||
<span class="list-namespace-title">{{ namespace(props.option.namespaceId) }} ></span>
|
||||
{{ props.option.title }}
|
||||
</template>
|
||||
</multiselect>
|
||||
|
@ -20,7 +21,7 @@
|
|||
<script>
|
||||
import ListService from '../../../services/list'
|
||||
import ListModel from '../../../models/list'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'listSearch',
|
||||
|
@ -31,11 +32,6 @@ export default {
|
|||
foundLists: [],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Multiselect,
|
||||
},
|
||||
|
@ -43,14 +39,6 @@ export default {
|
|||
this.listSerivce = new ListService()
|
||||
this.list = new ListModel()
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.list = newVal
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.list = this.value
|
||||
},
|
||||
methods: {
|
||||
findLists(query) {
|
||||
if (query === '') {
|
||||
|
@ -70,9 +58,7 @@ export default {
|
|||
this.$set(this, 'foundLists', [])
|
||||
},
|
||||
select(list) {
|
||||
this.list = list
|
||||
this.$emit('selected', list)
|
||||
this.$emit('input', list)
|
||||
},
|
||||
namespace(namespaceId) {
|
||||
const namespace = this.$store.getters['namespaces/getNamespaceById'](namespaceId)
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import priorites from '../../../models/constants/priorities'
|
||||
import priorites from '../../../models/priorities'
|
||||
|
||||
export default {
|
||||
name: 'priorityLabel',
|
||||
|
@ -44,6 +44,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/theme/variables/all';
|
||||
|
||||
.priority-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import priorites from '../../../models/constants/priorities'
|
||||
import priorites from '../../../models/priorities'
|
||||
|
||||
export default {
|
||||
name: 'prioritySelect',
|
||||
|
|
|
@ -122,10 +122,10 @@
|
|||
import TaskService from '../../../services/task'
|
||||
import TaskModel from '../../../models/task'
|
||||
import TaskRelationService from '../../../services/taskRelation'
|
||||
import relationKinds from '../../../models/constants/relationKinds'
|
||||
import relationKinds from '../../../models/relationKinds'
|
||||
import TaskRelationModel from '../../../models/taskRelation'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'relatedTasks',
|
||||
|
@ -269,6 +269,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/theme/variables/all';
|
||||
|
||||
.add-task-relation-button {
|
||||
margin-top: -3rem;
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import datepicker from '@/components/input/datepicker.vue'
|
||||
import datepicker from '@/components/input/datepicker'
|
||||
|
||||
export default {
|
||||
name: 'reminders',
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import repeatModes from '@/models/constants/taskRepeatModes'
|
||||
import repeatModes from '@/models/taskRepeatModes'
|
||||
|
||||
export default {
|
||||
name: 'repeatAfter',
|
||||
|
@ -62,7 +62,7 @@ export default {
|
|||
amount: 0,
|
||||
type: '',
|
||||
},
|
||||
repeatModes,
|
||||
repeatModes: repeatModes,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
@ -90,10 +90,6 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.task.repeatMode !== repeatModes.REPEAT_MODE_DEFAULT && this.repeatAfter.amount === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.task.repeatAfter = this.repeatAfter
|
||||
this.$emit('input', this.task)
|
||||
this.$emit('change')
|
||||
|
|
|
@ -135,7 +135,7 @@ export default {
|
|||
showListColor: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
theTask(newVal) {
|
||||
|
@ -178,13 +178,13 @@ export default {
|
|||
this.success({
|
||||
message: this.task.done ?
|
||||
this.$t('task.doneSuccess') :
|
||||
this.$t('task.undoneSuccess'),
|
||||
this.$t('task.undoneSuccess')
|
||||
}, [{
|
||||
title: 'Undo',
|
||||
callback: () => {
|
||||
this.task.done = !this.task.done
|
||||
this.markAsDone(!checked)
|
||||
},
|
||||
}
|
||||
}])
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -146,6 +146,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../styles/theme/variables/all';
|
||||
|
||||
.cropper {
|
||||
height: 80vh;
|
||||
background: transparent;
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
<template>
|
||||
<card :title="$t('user.export.title')">
|
||||
<p>
|
||||
{{ $t('user.export.description') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('user.export.descriptionPasswordRequired') }}
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label" for="currentPasswordDataExport">
|
||||
{{ $t('user.settings.currentPassword') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
:class="{'is-danger': errPasswordRequired}"
|
||||
id="currentPasswordDataExport"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-model="password"
|
||||
@keyup="() => errPasswordRequired = password === ''"
|
||||
ref="passwordInput"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errPasswordRequired">
|
||||
{{ $t('user.deletion.passwordRequired') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
:loading="dataExportService.loading"
|
||||
@click="requestDataExport()"
|
||||
class="is-fullwidth mt-4">
|
||||
{{ $t('user.export.request') }}
|
||||
</x-button>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataExportService from '../../../services/dataExport'
|
||||
|
||||
export default {
|
||||
name: 'data-export',
|
||||
data() {
|
||||
return {
|
||||
dataExportService: DataExportService,
|
||||
password: '',
|
||||
errPasswordRequired: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.dataExportService = new DataExportService()
|
||||
},
|
||||
methods: {
|
||||
requestDataExport() {
|
||||
if (this.password === '') {
|
||||
this.errPasswordRequired = true
|
||||
this.$refs.passwordInput.focus()
|
||||
return
|
||||
}
|
||||
|
||||
this.dataExportService.request(this.password)
|
||||
.then(() => {
|
||||
this.success({message: this.$t('user.export.success')})
|
||||
this.password = ''
|
||||
})
|
||||
.catch(e => this.error(e))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,138 +0,0 @@
|
|||
<template>
|
||||
<card :title="$t('user.deletion.title')" v-if="userDeletionEnabled">
|
||||
<template v-if="deletionScheduledAt !== null">
|
||||
<form @submit.prevent="cancelDeletion()">
|
||||
<p>
|
||||
{{
|
||||
$t('user.deletion.scheduled', {
|
||||
date: formatDateShort(deletionScheduledAt),
|
||||
dateSince: formatDateSince(deletionScheduledAt),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('user.deletion.scheduledCancelText') }}
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label" for="currentPasswordAccountDelete">
|
||||
{{ $t('user.settings.currentPassword') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
:class="{'is-danger': errPasswordRequired}"
|
||||
id="currentPasswordAccountDelete"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-model="password"
|
||||
@keyup="() => errPasswordRequired = password === ''"
|
||||
ref="passwordInput"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errPasswordRequired">
|
||||
{{ $t('user.deletion.passwordRequired') }}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-button
|
||||
:loading="accountDeleteService.loading"
|
||||
@click="cancelDeletion()"
|
||||
class="is-fullwidth mt-4">
|
||||
{{ $t('user.deletion.scheduledCancelConfirm') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<form @submit.prevent="deleteAccount()">
|
||||
<p>
|
||||
{{ $t('user.deletion.text1') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('user.deletion.text2') }}
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="label" for="currentPasswordAccountDelete">
|
||||
{{ $t('user.settings.currentPassword') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
:class="{'is-danger': errPasswordRequired}"
|
||||
id="currentPasswordAccountDelete"
|
||||
:placeholder="$t('user.settings.currentPasswordPlaceholder')"
|
||||
type="password"
|
||||
v-model="password"
|
||||
@keyup="() => errPasswordRequired = password === ''"
|
||||
ref="passwordInput"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="errPasswordRequired">
|
||||
{{ $t('user.deletion.passwordRequired') }}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<x-button
|
||||
:loading="accountDeleteService.loading"
|
||||
@click="deleteAccount()"
|
||||
class="is-fullwidth mt-4 is-danger">
|
||||
{{ $t('user.deletion.confirm') }}
|
||||
</x-button>
|
||||
</template>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccountDeleteService from '../../../services/accountDelete'
|
||||
import {mapState} from 'vuex'
|
||||
import {parseDateOrNull} from '../../../helpers/parseDateOrNull'
|
||||
|
||||
export default {
|
||||
name: 'user-settings-deletion',
|
||||
data() {
|
||||
return {
|
||||
accountDeleteService: AccountDeleteService,
|
||||
password: '',
|
||||
errPasswordRequired: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.accountDeleteService = new AccountDeleteService()
|
||||
},
|
||||
computed: mapState({
|
||||
userDeletionEnabled: state => state.config.userDeletionEnabled,
|
||||
deletionScheduledAt: state => parseDateOrNull(state.auth.info.deletionScheduledAt),
|
||||
}),
|
||||
methods: {
|
||||
deleteAccount() {
|
||||
if (this.password === '') {
|
||||
this.errPasswordRequired = true
|
||||
this.$refs.passwordInput.focus()
|
||||
return
|
||||
}
|
||||
|
||||
this.accountDeleteService.request(this.password)
|
||||
.then(() => {
|
||||
this.success({message: this.$t('user.deletion.requestSuccess')})
|
||||
this.password = ''
|
||||
})
|
||||
.catch(e => this.error(e))
|
||||
},
|
||||
cancelDeletion() {
|
||||
if (this.password === '') {
|
||||
this.errPasswordRequired = true
|
||||
this.$refs.passwordInput.focus()
|
||||
return
|
||||
}
|
||||
|
||||
this.accountDeleteService.cancel(this.password)
|
||||
.then(() => {
|
||||
this.success({message: this.$t('user.deletion.scheduledCancelSuccess')})
|
||||
this.$store.dispatch('auth/refreshUserInfo')
|
||||
this.password = ''
|
||||
})
|
||||
.catch(e => this.error(e))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,18 @@
|
|||
export const applyDrag = (arr, dragResult) => {
|
||||
const {removedIndex, addedIndex, payload} = dragResult
|
||||
if (removedIndex === null && addedIndex === null) return arr
|
||||
|
||||
const result = [...arr]
|
||||
// The payload comes from the task itself
|
||||
let itemToAdd = payload
|
||||
|
||||
if (removedIndex !== null) {
|
||||
itemToAdd = result.splice(removedIndex, 1)[0]
|
||||
}
|
||||
|
||||
if (addedIndex !== null) {
|
||||
result.splice(addedIndex, 0, itemToAdd)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import {HTTPFactory} from '@/http-common'
|
||||
import {AxiosResponse} from 'axios'
|
||||
|
||||
let savedToken: string | null = null
|
||||
|
||||
/**
|
||||
* Saves a token while optionally saving it to lacal storage. This is used when viewing a link share:
|
||||
* It enables viewing multiple link shares indipendently from each in multiple tabs other without overriding any other open ones.
|
||||
* @param token
|
||||
* @param persist
|
||||
*/
|
||||
export const saveToken = (token: string, persist: boolean) => {
|
||||
savedToken = token
|
||||
if (persist) {
|
||||
localStorage.setItem('token', token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a saved token. If there is one saved in memory it will use that before anything else.
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export const getToken = (): string | null => {
|
||||
if (savedToken !== null) {
|
||||
return savedToken
|
||||
}
|
||||
|
||||
savedToken = localStorage.getItem('token')
|
||||
return savedToken
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all tokens everywhere.
|
||||
*/
|
||||
export const removeToken = () => {
|
||||
savedToken = null
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes an auth token while ensuring it is updated everywhere.
|
||||
* @returns {Promise<AxiosResponse<any>>}
|
||||
*/
|
||||
export const refreshToken = (persist: boolean): Promise<AxiosResponse> => {
|
||||
const HTTP = HTTPFactory()
|
||||
return HTTP.post('user/token', null, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
})
|
||||
.then(r => {
|
||||
saveToken(r.data.token, persist)
|
||||
return Promise.resolve(r)
|
||||
})
|
||||
.catch(e => {
|
||||
// eslint-disable-next-line
|
||||
console.log('Error renewing token: ', e)
|
||||
return Promise.reject(e)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
export const calculateItemPosition = (positionBefore: number | null, positionAfter: number | null): number => {
|
||||
if (positionBefore === null && positionAfter === null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// If there is no task before, our task is the first task in which case we let it have half of the position of the task after it
|
||||
if (positionBefore === null && positionAfter !== null) {
|
||||
return positionAfter / 2
|
||||
}
|
||||
|
||||
// If there is no task after it, we just add 2^16 to the last position to have enough room in the future
|
||||
if (positionBefore !== null && positionAfter === null) {
|
||||
return positionBefore + Math.pow(2, 16)
|
||||
}
|
||||
|
||||
// If we have both a task before and after it, we acually calculate the position
|
||||
// @ts-ignore - can never be null but TS does not seem to understand that
|
||||
return positionBefore + (positionAfter - positionBefore) / 2
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import {calculateItemPosition} from './calculateItemPosition'
|
||||
|
||||
it('should calculate the task position', () => {
|
||||
const result = calculateItemPosition(10, 100)
|
||||
expect(result).toBe(55)
|
||||
})
|
||||
it('should return 0 if no position was provided', () => {
|
||||
const result = calculateItemPosition(null, null)
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
it('should calculate the task position for the first task', () => {
|
||||
const result = calculateItemPosition(null, 100)
|
||||
expect(result).toBe(50)
|
||||
})
|
||||
it('should calculate the task position for the last task', () => {
|
||||
const result = calculateItemPosition(10, null)
|
||||
expect(result).toBe(65546)
|
||||
})
|
|
@ -1,7 +0,0 @@
|
|||
export const downloadBlob = (url: string, filename: string) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', filename)
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export function findIndexById(array : [], id : string | number) {
|
||||
return array.findIndex(({id: currentId}) => currentId === id)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
export const getListTitle = (l, $t) => {
|
||||
if (l.id === -1) {
|
||||
return $t('list.pseudo.favorites.title')
|
||||
return $t('list.pseudo.favorites.title');
|
||||
}
|
||||
return l.title
|
||||
return l.title;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
export const getNamespaceTitle = (n, $t) => {
|
||||
if (n.id === -1) {
|
||||
return $t('namespace.pseudo.sharedLists.title')
|
||||
return $t('namespace.pseudo.sharedLists.title');
|
||||
}
|
||||
if (n.id === -2) {
|
||||
return $t('namespace.pseudo.favorites.title')
|
||||
return $t('namespace.pseudo.favorites.title');
|
||||
}
|
||||
if (n.id === -3) {
|
||||
return $t('namespace.pseudo.savedFilters.title')
|
||||
return $t('namespace.pseudo.savedFilters.title');
|
||||
}
|
||||
return n.title
|
||||
return n.title;
|
||||
}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
export interface Migrator {
|
||||
name: string
|
||||
identifier: string
|
||||
isFileMigrator?: boolean
|
||||
}
|
||||
|
||||
export const getMigratorFromSlug = (slug: string): Migrator => {
|
||||
switch (slug) {
|
||||
case 'wunderlist':
|
||||
return {
|
||||
name: 'Wunderlist',
|
||||
identifier: 'wunderlist',
|
||||
}
|
||||
case 'todoist':
|
||||
return {
|
||||
name: 'Todoist',
|
||||
identifier: 'todoist',
|
||||
}
|
||||
case 'trello':
|
||||
return {
|
||||
name: 'Trello',
|
||||
identifier: 'trello',
|
||||
}
|
||||
case 'microsoft-todo':
|
||||
return {
|
||||
name: 'Microsoft Todo',
|
||||
identifier: 'microsoft-todo',
|
||||
}
|
||||
case 'vikunja-file':
|
||||
return {
|
||||
name: 'Vikunja Export',
|
||||
identifier: 'vikunja-file',
|
||||
isFileMigrator: true,
|
||||
}
|
||||
default:
|
||||
throw Error('Unknown migrator slug ' + slug)
|
||||
}
|
||||
}
|
|
@ -1,38 +1,18 @@
|
|||
import {parseDate} from '../helpers/time/parseDate'
|
||||
import _priorities from '../models/constants/priorities.json'
|
||||
import {parseDate} from './time/parseDate'
|
||||
import priorities from '../models/priorities.json'
|
||||
|
||||
const LABEL_PREFIX: string = '@'
|
||||
const LIST_PREFIX: string = '#'
|
||||
const PRIORITY_PREFIX: string = '!'
|
||||
const ASSIGNEE_PREFIX: string = '+'
|
||||
|
||||
const priorities: Priorites = _priorities
|
||||
|
||||
interface Priorites {
|
||||
UNSET: number,
|
||||
LOW: number,
|
||||
MEDIUM: number,
|
||||
HIGH: number,
|
||||
URGENT: number,
|
||||
DO_NOW: number,
|
||||
}
|
||||
|
||||
interface ParsedTaskText {
|
||||
text: string,
|
||||
date: Date | null,
|
||||
labels: string[],
|
||||
list: string | null,
|
||||
priority: number | null,
|
||||
assignees: string[],
|
||||
}
|
||||
const LABEL_PREFIX = '@'
|
||||
const LIST_PREFIX = '#'
|
||||
const PRIORITY_PREFIX = '!'
|
||||
const ASSIGNEE_PREFIX = '+'
|
||||
|
||||
/**
|
||||
* Parses task text for dates, assignees, labels, lists, priorities and returns an object with all found intents.
|
||||
*
|
||||
* @param text
|
||||
*/
|
||||
export const parseTaskText = (text: string): ParsedTaskText => {
|
||||
const result: ParsedTaskText = {
|
||||
export const parseTaskText = text => {
|
||||
const result = {
|
||||
text: text,
|
||||
date: null,
|
||||
labels: [],
|
||||
|
@ -43,7 +23,7 @@ export const parseTaskText = (text: string): ParsedTaskText => {
|
|||
|
||||
result.labels = getItemsFromPrefix(text, LABEL_PREFIX)
|
||||
|
||||
const lists: string[] = getItemsFromPrefix(text, LIST_PREFIX)
|
||||
const lists = getItemsFromPrefix(text, LIST_PREFIX)
|
||||
result.list = lists.length > 0 ? lists[0] : null
|
||||
|
||||
result.priority = getPriority(text)
|
||||
|
@ -57,8 +37,8 @@ export const parseTaskText = (text: string): ParsedTaskText => {
|
|||
return cleanupResult(result)
|
||||
}
|
||||
|
||||
const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
||||
const items: string[] = []
|
||||
const getItemsFromPrefix = (text, prefix) => {
|
||||
const items = []
|
||||
|
||||
const itemParts = text.split(prefix)
|
||||
itemParts.forEach((p, index) => {
|
||||
|
@ -68,10 +48,10 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
|||
}
|
||||
|
||||
let labelText
|
||||
if (p.charAt(0) === '\'') {
|
||||
labelText = p.split('\'')[1]
|
||||
} else if (p.charAt(0) === '"') {
|
||||
labelText = p.split('"')[1]
|
||||
if (p.charAt(0) === `'`) {
|
||||
labelText = p.split(`'`)[1]
|
||||
} else if (p.charAt(0) === `"`) {
|
||||
labelText = p.split(`"`)[1]
|
||||
} else {
|
||||
// Only until the next space
|
||||
labelText = p.split(' ')[0]
|
||||
|
@ -82,15 +62,15 @@ const getItemsFromPrefix = (text: string, prefix: string): string[] => {
|
|||
return Array.from(new Set(items))
|
||||
}
|
||||
|
||||
const getPriority = (text: string): number | null => {
|
||||
const getPriority = text => {
|
||||
const ps = getItemsFromPrefix(text, PRIORITY_PREFIX)
|
||||
if (ps.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const p of ps) {
|
||||
for (const pi of Object.values(priorities)) {
|
||||
if (pi === parseInt(p)) {
|
||||
for (const pi in priorities) {
|
||||
if (priorities[pi] === parseInt(p)) {
|
||||
return parseInt(p)
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +79,7 @@ const getPriority = (text: string): number | null => {
|
|||
return null
|
||||
}
|
||||
|
||||
const cleanupItemText = (text: string, items: string[], prefix: string): string => {
|
||||
const cleanupItemText = (text, items, prefix) => {
|
||||
items.forEach(l => {
|
||||
text = text
|
||||
.replace(`${prefix}'${l}' `, '')
|
||||
|
@ -112,10 +92,10 @@ const cleanupItemText = (text: string, items: string[], prefix: string): string
|
|||
return text
|
||||
}
|
||||
|
||||
const cleanupResult = (result: ParsedTaskText): ParsedTaskText => {
|
||||
const cleanupResult = result => {
|
||||
result.text = cleanupItemText(result.text, result.labels, LABEL_PREFIX)
|
||||
result.text = result.list !== null ? cleanupItemText(result.text, [result.list], LIST_PREFIX) : result.text
|
||||
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], PRIORITY_PREFIX) : result.text
|
||||
result.text = cleanupItemText(result.text, [result.list], LIST_PREFIX)
|
||||
result.text = cleanupItemText(result.text, [result.priority], PRIORITY_PREFIX)
|
||||
result.text = cleanupItemText(result.text, result.assignees, ASSIGNEE_PREFIX)
|
||||
result.text = result.text.trim()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue