forked from vikunja/frontend
Compare commits
60 Commits
main
...
translatio
Author | SHA1 | Date | |
---|---|---|---|
|
69590246df | ||
|
b203e4a169 | ||
|
c5f8ed629a | ||
|
08ed54ab4e | ||
2370115c35 | |||
86ca6c29c5 | |||
f6b4b44743 | |||
|
b54fae513a | ||
d350b02aca | |||
1238528e2b | |||
8de194bc06 | |||
9800e1701d | |||
8e6633f70f | |||
eaca985d44 | |||
1d5def2d8e | |||
7d5077cd8f | |||
b24365640f | |||
|
024af54cd1 | ||
5cf1fb831a | |||
|
eb6ade1fac | ||
|
49fa8dd5ff | ||
|
004484fbd7 | ||
86e9cfdf4b | |||
|
32dd9bf138 | ||
|
7476949852 | ||
|
f7e24f9df3 | ||
82b756cd99 | |||
|
6ade8c6607 | ||
|
da71cf7220 | ||
|
4a7d0d5b7b | ||
|
76f67f60bc | ||
|
812d1ba560 | ||
|
aef4792be5 | ||
e2959f210d | |||
33ff902c6c | |||
fca4b93002 | |||
dc41288ec1 | |||
|
2fd47b585d | ||
|
44a4e08d0d | ||
|
db31574858 | ||
|
345f02b66a | ||
|
53a4e463f2 | ||
|
d3586a3d5c | ||
3aa8488dc4 | |||
|
b0827e2ba8 | ||
|
4123d739d9 | ||
d55fdbf223 | |||
2b8884c39a | |||
b93d853022 | |||
3db06bc81b | |||
3416c2598e | |||
2d754f0aac | |||
|
01669831e5 | ||
b25cea2180 | |||
|
be86427374 | ||
|
4dbec1acab | ||
e096de57d3 | |||
|
4ba6261549 | ||
|
a707931c55 | ||
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
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
BIN
public/fonts/open-sans-v15-latin-700.ttf
Normal file
BIN
public/fonts/open-sans-v15-latin-700.ttf
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-700.woff
Normal file
BIN
public/fonts/open-sans-v15-latin-700.woff
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-700italic.ttf
Normal file
BIN
public/fonts/open-sans-v15-latin-700italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-700italic.woff
Normal file
BIN
public/fonts/open-sans-v15-latin-700italic.woff
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-italic.ttf
Normal file
BIN
public/fonts/open-sans-v15-latin-italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-italic.woff
Normal file
BIN
public/fonts/open-sans-v15-latin-italic.woff
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-regular.ttf
Normal file
BIN
public/fonts/open-sans-v15-latin-regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/open-sans-v15-latin-regular.woff
Normal file
BIN
public/fonts/open-sans-v15-latin-regular.woff
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-300.ttf
Normal file
BIN
public/fonts/quicksand-v7-latin-300.ttf
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-300.woff
Normal file
BIN
public/fonts/quicksand-v7-latin-300.woff
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-500.ttf
Normal file
BIN
public/fonts/quicksand-v7-latin-500.ttf
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-500.woff
Normal file
BIN
public/fonts/quicksand-v7-latin-500.woff
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-700.ttf
Normal file
BIN
public/fonts/quicksand-v7-latin-700.ttf
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-700.woff
Normal file
BIN
public/fonts/quicksand-v7-latin-700.woff
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-regular.ttf
Normal file
BIN
public/fonts/quicksand-v7-latin-regular.ttf
Normal file
Binary file not shown.
BIN
public/fonts/quicksand-v7-latin-regular.woff
Normal file
BIN
public/fonts/quicksand-v7-latin-regular.woff
Normal file
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 |
36
public/index.html
Normal file
36
public/index.html
Normal file
|
@ -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>
|
||||
|
|
3
src/ServiceWorker/events.json
Normal file
3
src/ServiceWorker/events.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"SW_UPDATED": "swUpdated"
|
||||
}
|
112
src/ServiceWorker/sw.js
Normal file
112
src/ServiceWorker/sw.js
Normal file
|
@ -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>
|
18
src/helpers/applyDrag.js
Normal file
18
src/helpers/applyDrag.js
Normal file
|
@ -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
Block a user