Compare commits
84 Commits
renovate/l
...
main
Author | SHA1 | Date | |
---|---|---|---|
652d3c7384 | |||
447641c222 | |||
362be53a47 | |||
46eabdfe6b | |||
a0c5a464a5 | |||
e78ab476fc | |||
aebb047d18 | |||
7bb110b20e | |||
f148a43390 | |||
aac70d3823 | |||
21126793ab | |||
b057fb2784 | |||
58c7da019d | |||
70f48eaaca | |||
6cc75928d8 | |||
dc360d4a18 | |||
45ca0602f5 | |||
9d39ccf15c | |||
28e83325d7 | |||
aff48ddd9d | |||
5b2a9a42c0 | |||
45f5d522d1 | |||
4f27e4a477 | |||
d0dc86fd58 | |||
0484923b8a | |||
5f2fb01e90 | |||
bd18524f36 | |||
7375a87f2f | |||
ccff276397 | |||
30b21fc11c | |||
7c98ddc20b | |||
6ba02a0f10 | |||
676d2b6215 | |||
|
85e612451f | ||
d411de99f1 | |||
228d652b03 | |||
b3e2107503 | |||
a579a8e65f | |||
ee980e2a00 | |||
394dbe0055 | |||
30d599369f | |||
631b02d2ee | |||
326bfb557a | |||
cd0149ef69 | |||
78d4a518a3 | |||
3c1041902e | |||
e3cae0ed7f | |||
fc8bd6a9ca | |||
5a6e5619e3 | |||
9c9f806e62 | |||
67216579bc | |||
a8df935ddb | |||
bb4746f226 | |||
31590236aa | |||
00d48a6178 | |||
5169cca8d8 | |||
255a7d565c | |||
8dbaee5dfb | |||
69b0b19482 | |||
eae89d37f1 | |||
7d19859816 | |||
c7b70844c6 | |||
b8c21c2ade | |||
57c99a22a0 | |||
8ea97f3ffc | |||
0b3604d167 | |||
c5ba7fcb73 | |||
5a25685d53 | |||
da311fce9e | |||
0fdf1ca027 | |||
f8e907a8c1 | |||
af7ca8ad8f | |||
92f7d9ded5 | |||
41ccaea78b | |||
c5696f3e2a | |||
898707664c | |||
d0b5bef68a | |||
e395d4efdb | |||
ce54132868 | |||
07d4d1e537 | |||
a701b0452e | |||
af65efcd27 | |||
dc2afb9e8d | |||
e123d4f825 |
|
@ -1,5 +1,5 @@
|
|||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution")
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
'root': true,
|
||||
|
@ -7,52 +7,54 @@ module.exports = {
|
|||
'browser': true,
|
||||
'es2022': true,
|
||||
'node': true,
|
||||
'vue/setup-compiler-macros': true,
|
||||
},
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
],
|
||||
'rules': {
|
||||
'vue/html-quotes': [
|
||||
'error',
|
||||
'double',
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single',
|
||||
],
|
||||
'comma-dangle': [
|
||||
'error',
|
||||
'always-multiline',
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'never',
|
||||
],
|
||||
'quotes': ['error', 'single'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'semi': ['error', 'never'],
|
||||
|
||||
// see https://segmentfault.com/q/1010000040813116/a-1020000041134455 (original in chinese)
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
||||
'vue/v-on-event-hyphenation': ['warn', 'never', { 'autofix': true }],
|
||||
'vue/multi-word-component-names': 'off',
|
||||
|
||||
'vue/multi-word-component-names': 0,
|
||||
// disabled until we have support for reactivityTransform
|
||||
// See https://github.com/vuejs/eslint-plugin-vue/issues/1948
|
||||
// see also setting in `vite.config`
|
||||
'vue/no-setup-props-destructure': 0,
|
||||
// uncategorized rules:
|
||||
'vue/component-api-style': ['error', ['script-setup']],
|
||||
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
|
||||
'vue/custom-event-name-casing': ['error', 'camelCase'],
|
||||
'vue/define-macros-order': 'error',
|
||||
'vue/match-component-file-name': ['error', {
|
||||
'extensions': ['.js', '.jsx', '.ts', '.tsx', '.vue'],
|
||||
'shouldMatchCase': true,
|
||||
}],
|
||||
'vue/no-boolean-default': ['warn', 'default-false'],
|
||||
'vue/match-component-import-name': 'error',
|
||||
'vue/prefer-separate-static-class': 'warn',
|
||||
|
||||
'vue/padding-line-between-blocks': 'error',
|
||||
'vue/next-tick-style': ['error', 'promise'],
|
||||
'vue/block-lang': [
|
||||
'error',
|
||||
{ 'script': { 'lang': 'ts' } },
|
||||
],
|
||||
'vue/no-required-prop-with-default': ['error', { 'autofix': true }],
|
||||
'vue/no-duplicate-attr-inheritance': 'error',
|
||||
'vue/no-empty-component-block': 'error',
|
||||
'vue/html-indent': ['error', 'tab'],
|
||||
|
||||
// vue3
|
||||
'vue/no-ref-object-destructure': 'error',
|
||||
},
|
||||
'parser': 'vue-eslint-parser',
|
||||
'parserOptions': {
|
||||
'parser': '@typescript-eslint/parser',
|
||||
'ecmaVersion': 2022,
|
||||
'sourceType': 'module',
|
||||
'ecmaVersion': 'latest',
|
||||
},
|
||||
'ignorePatterns': [
|
||||
'*.test.*',
|
||||
'cypress/*',
|
||||
],
|
||||
'globals': {
|
||||
'defineProps': 'readonly',
|
||||
},
|
||||
}
|
110
CHANGELOG.md
110
CHANGELOG.md
|
@ -9,6 +9,116 @@ All releases can be found on https://code.vikunja.io/frontend/releases.
|
|||
|
||||
The releases aim at the api versions which is why there are missing versions.
|
||||
|
||||
## [0.22.1] - 2024-01-28
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(auth)* Correctly construct redirect url from current window href
|
||||
* *(ci)* Use working crowdin image
|
||||
* *(ci)* Use working image for crowdin update step
|
||||
* *(ci)* Use working crowdin image
|
||||
* *(color picker)* When picking a color, the color picker should not be black afterwards
|
||||
* *(editor)* List icons
|
||||
* *(editor)* Use higher-contrast colors for links and code
|
||||
* *(editor)* Don't bubble up changes when no changes were made
|
||||
* *(editor)* Focus the editor when clicking on the whole edit container
|
||||
* *(editor)* Render images without crashing
|
||||
* *(editor)* Use a stable image id to prevent constant re-rendering
|
||||
* *(editor)* Use manual input prompt instead of window.prompt
|
||||
* *(filter)* Validate filter title field after loading a filter for edit
|
||||
* *(kanban)* Ensure text and icon color only depends on the card background, not on the color scheme
|
||||
* *(kanban)* Make sure the checklist summary uses the correct text color
|
||||
* *(kanban)* Make sure spacing between assignees and other task details works out evenly
|
||||
* *(labels)* Make color reset work
|
||||
* *(labels)* Text and background combination in dark mode
|
||||
* *(notifications)* Unread indicator spacing
|
||||
* *(notifications)* Always left-align notification text
|
||||
* *(notifications)* Read indicator size
|
||||
* *(openid)* Use the full path when building the redirect url, not only the host
|
||||
* *(openid)* Use the calculated redirect url when authenticating with openid providers
|
||||
* *(project)* Always use the appropriate color for task estimate during deletion dialoge
|
||||
* *(quick add magic)* Ensure month is removed from task text
|
||||
* *(table view)* Make sure popup does not overlap
|
||||
* *(task)* Don't immediately re-trigger date change when nothing changed
|
||||
* *(task)* Bubble date changes from the picker up
|
||||
* *(task)* Update due date when marking a task done
|
||||
* *(task)* Don't show edit button when the user does not have permission to edit the task
|
||||
* *(task)* Don't show assignee edit buttons and input when the user does not have the permission to edit
|
||||
* *(tasks)* Make sure tasks show up if their parent task is not available in the current view
|
||||
* *(tasks)* Don't load tasks multiple times when viewing list or gantt view
|
||||
* *(test)* Make date assertion not brittle
|
||||
* Lint ([5e991f3](5e991f3024f7856420614171ec66468eb2e2df63))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v2 (#3862)
|
||||
* *(deps)* Update pnpm to v8.14.0
|
||||
* *(deps)* Update dependency vue to v3.4.7 (#3873)
|
||||
* *(deps)* Update dependency axios to v1.6.5 (#3871)
|
||||
* *(deps)* Update dependency date-fns to v3 (#3857)
|
||||
* *(deps)* Update dev-dependencies (#3861)
|
||||
* *(deps)* Update dependency @kyvg/vue3-notification to v3.1.3 (#3864)
|
||||
* *(deps)* Update dependency node to v20.11.0
|
||||
* *(deps)* Update dependency vue-i18n to v9.9.0 (#3880)
|
||||
* *(deps)* Update dependency dompurify to v3.0.8 (#3881)
|
||||
* *(deps)* Update dependency floating-vue to v2.0.0 (#3883)
|
||||
* *(deps)* Update tiptap to v2.1.15 (#3884)
|
||||
* *(deps)* Update vueuse to v10.7.1 (#3872)
|
||||
* *(deps)* Update pnpm to v8.14.1 (#3885)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.93.0 (#3859)
|
||||
* *(deps)* Update dependency floating-vue to v5 (#3887)
|
||||
* *(deps)* Update dependency vue to v3.4.8 (#3886)
|
||||
* *(deps)* Update node.js to v20.11 (#3888)
|
||||
* *(deps)* Increase renovate timeout
|
||||
* *(deps)* Update tiptap to v2.1.16 (#3892)
|
||||
* *(deps)* Pin node.js (#3895)
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency sortablejs to v1.15.2
|
||||
* *(deps)* Update vueuse to v10.7.2
|
||||
* *(deps)* Update dependency floating-vue to v5.1.0
|
||||
* *(deps)* Update dependency vue to v3.4.14
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies (major) (#3890)
|
||||
* *(deps)* Update dependency floating-vue to v5.1.1
|
||||
* *(deps)* Update dependency floating-vue to v5.2.0
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency vue to v3.4.15
|
||||
* *(deps)* Update dependency happy-dom to v13.2.0
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.94.1
|
||||
* *(deps)* Update dependency vite to v5.0.12
|
||||
* *(deps)* Update dependency date-fns to v3.3.0
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update pnpm to v8.14.2
|
||||
* *(deps)* Update dependency date-fns to v3.3.1
|
||||
* *(deps)* Update dev-dependencies to v6.19.1
|
||||
* *(deps)* Update pnpm to v8.14.3
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.95.0
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency axios to v1.6.6
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.97.0
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.98.0
|
||||
* *(deps)* Update dependency axios to v1.6.7
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
|
||||
### Features
|
||||
|
||||
* *(reminders)* Show reminders in notifications bar
|
||||
* Datepicker locale support (#3878) ([92f7d9d](92f7d9ded5d56b95ba7d647eba01372f6ef682ad))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(perf)* Import some modules dynamically (#3179)
|
||||
* Only show webhooks overview table when there are webhooks ([326bfb5](326bfb557ab359fa154b163f5dd957928f46d3ec))
|
||||
* Only show webhooks overview table when there are webhooks ([631b02d](631b02d2eedc4a403b7c55f1c56ceaeca5379bf5))
|
||||
|
||||
## [0.22.0] - 2023-12-19
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
# This repository was merged with the api and is now archived
|
||||
|
||||
You can find the new (old) code over on [vikunja/vikunja](https://kolaente.dev/vikunja/vikunja).
|
||||
|
||||
# Web frontend for Vikunja
|
||||
|
||||
> The todo app to organize your life.
|
||||
|
||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||
[![Download](https://img.shields.io/badge/download-v0.22.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.22.1-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||
|
||||
This is the web frontend for Vikunja, written in Vue.js.
|
||||
|
@ -25,7 +29,7 @@ export DOCKER_BUILDKIT=1
|
|||
docker build -t vikunja/frontend .
|
||||
```
|
||||
|
||||
Refer to Refer [to multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for the different platform.
|
||||
Refer to [multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for different platforms.
|
||||
|
||||
## Project setup
|
||||
|
||||
|
|
|
@ -562,7 +562,7 @@ describe('Task', () => {
|
|||
.click()
|
||||
|
||||
const today = new Date()
|
||||
const day = today.toLocaleString('default', {day: '2-digit'})
|
||||
const day = today.toLocaleString('default', {day: 'numeric'})
|
||||
const month = today.toLocaleString('default', {month: 'short'})
|
||||
const year = today.toLocaleString('default', {year: 'numeric'})
|
||||
const date = `${day} ${month} ${year}, 12:00:00`
|
||||
|
@ -605,7 +605,7 @@ describe('Task', () => {
|
|||
.click()
|
||||
|
||||
const today = new Date()
|
||||
const day = today.toLocaleString('default', {day: '2-digit'})
|
||||
const day = today.toLocaleString('default', {day: 'numeric'})
|
||||
const month = today.toLocaleString('default', {month: 'short'})
|
||||
const year = today.toLocaleString('default', {year: 'numeric'})
|
||||
const date = `${day} ${month} ${year}, 12:00:00`
|
||||
|
|
134
package.json
134
package.json
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@8.14.1",
|
||||
"packageManager": "pnpm@8.15.1",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -22,6 +22,7 @@
|
|||
"gantt",
|
||||
"kanban"
|
||||
],
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"preview": "vite preview --port 4173",
|
||||
|
@ -29,7 +30,8 @@
|
|||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
||||
"build:dev": "vite build --mode development --outDir dist-dev/",
|
||||
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
|
||||
"lint": "eslint 'src/**/*.{js,ts,vue}'",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
"test:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
|
||||
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
|
@ -48,61 +50,61 @@
|
|||
"@fortawesome/fontawesome-svg-core": "6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.1",
|
||||
"@fortawesome/vue-fontawesome": "3.0.5",
|
||||
"@fortawesome/vue-fontawesome": "3.0.6",
|
||||
"@github/hotkey": "3.1.0",
|
||||
"@infectoone/vue-ganttastic": "2.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "2.0.0",
|
||||
"@kyvg/vue3-notification": "3.1.3",
|
||||
"@sentry/tracing": "7.93.0",
|
||||
"@sentry/vue": "7.93.0",
|
||||
"@tiptap/core": "2.1.16",
|
||||
"@tiptap/extension-blockquote": "2.1.16",
|
||||
"@tiptap/extension-bold": "2.1.16",
|
||||
"@tiptap/extension-bullet-list": "2.1.16",
|
||||
"@tiptap/extension-code": "2.1.16",
|
||||
"@tiptap/extension-code-block-lowlight": "2.1.16",
|
||||
"@tiptap/extension-document": "2.1.16",
|
||||
"@tiptap/extension-dropcursor": "2.1.16",
|
||||
"@tiptap/extension-gapcursor": "2.1.16",
|
||||
"@tiptap/extension-hard-break": "2.1.16",
|
||||
"@tiptap/extension-heading": "2.1.16",
|
||||
"@tiptap/extension-history": "2.1.16",
|
||||
"@tiptap/extension-horizontal-rule": "2.1.16",
|
||||
"@tiptap/extension-image": "2.1.16",
|
||||
"@tiptap/extension-italic": "2.1.16",
|
||||
"@tiptap/extension-link": "2.1.16",
|
||||
"@tiptap/extension-list-item": "2.1.16",
|
||||
"@tiptap/extension-ordered-list": "2.1.16",
|
||||
"@tiptap/extension-paragraph": "2.1.16",
|
||||
"@tiptap/extension-placeholder": "2.1.16",
|
||||
"@tiptap/extension-strike": "2.1.16",
|
||||
"@tiptap/extension-table": "2.1.16",
|
||||
"@tiptap/extension-table-cell": "2.1.16",
|
||||
"@tiptap/extension-table-header": "2.1.16",
|
||||
"@tiptap/extension-table-row": "2.1.16",
|
||||
"@tiptap/extension-task-item": "2.1.16",
|
||||
"@tiptap/extension-task-list": "2.1.16",
|
||||
"@tiptap/extension-text": "2.1.16",
|
||||
"@tiptap/extension-typography": "2.1.16",
|
||||
"@tiptap/extension-underline": "2.1.16",
|
||||
"@tiptap/pm": "2.1.16",
|
||||
"@tiptap/suggestion": "2.1.16",
|
||||
"@tiptap/vue-3": "2.1.16",
|
||||
"@kyvg/vue3-notification": "3.1.4",
|
||||
"@sentry/tracing": "7.100.1",
|
||||
"@sentry/vue": "7.100.1",
|
||||
"@tiptap/core": "2.2.1",
|
||||
"@tiptap/extension-blockquote": "2.2.1",
|
||||
"@tiptap/extension-bold": "2.2.1",
|
||||
"@tiptap/extension-bullet-list": "2.2.1",
|
||||
"@tiptap/extension-code": "2.2.1",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.1",
|
||||
"@tiptap/extension-document": "2.2.1",
|
||||
"@tiptap/extension-dropcursor": "2.2.1",
|
||||
"@tiptap/extension-gapcursor": "2.2.1",
|
||||
"@tiptap/extension-hard-break": "2.2.1",
|
||||
"@tiptap/extension-heading": "2.2.1",
|
||||
"@tiptap/extension-history": "2.2.1",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.1",
|
||||
"@tiptap/extension-image": "2.2.1",
|
||||
"@tiptap/extension-italic": "2.2.1",
|
||||
"@tiptap/extension-link": "2.2.1",
|
||||
"@tiptap/extension-list-item": "2.2.1",
|
||||
"@tiptap/extension-ordered-list": "2.2.1",
|
||||
"@tiptap/extension-paragraph": "2.2.1",
|
||||
"@tiptap/extension-placeholder": "2.2.1",
|
||||
"@tiptap/extension-strike": "2.2.1",
|
||||
"@tiptap/extension-table": "2.2.1",
|
||||
"@tiptap/extension-table-cell": "2.2.1",
|
||||
"@tiptap/extension-table-header": "2.2.1",
|
||||
"@tiptap/extension-table-row": "2.2.1",
|
||||
"@tiptap/extension-task-item": "2.2.1",
|
||||
"@tiptap/extension-task-list": "2.2.1",
|
||||
"@tiptap/extension-text": "2.2.1",
|
||||
"@tiptap/extension-typography": "2.2.1",
|
||||
"@tiptap/extension-underline": "2.2.1",
|
||||
"@tiptap/pm": "2.2.1",
|
||||
"@tiptap/suggestion": "2.2.1",
|
||||
"@tiptap/vue-3": "2.2.1",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@vueuse/core": "10.7.2",
|
||||
"@vueuse/router": "10.7.2",
|
||||
"axios": "1.6.5",
|
||||
"axios": "1.6.7",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"date-fns": "3.2.0",
|
||||
"date-fns": "3.3.1",
|
||||
"dayjs": "1.11.10",
|
||||
"dompurify": "3.0.8",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
"floating-vue": "5.1.0",
|
||||
"floating-vue": "5.2.2",
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lodash.debounce": "4.0.8",
|
||||
|
@ -112,11 +114,11 @@
|
|||
"snake-case": "3.0.4",
|
||||
"sortablejs": "1.15.2",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.3.2",
|
||||
"vue": "3.4.14",
|
||||
"ufo": "1.4.0",
|
||||
"vue": "3.4.15",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.3",
|
||||
"vue-i18n": "9.9.0",
|
||||
"vue-i18n": "9.9.1",
|
||||
"vue-router": "4.2.5",
|
||||
"workbox-precaching": "7.0.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
|
@ -125,10 +127,10 @@
|
|||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.0.7",
|
||||
"@cypress/vue": "6.0.0",
|
||||
"@faker-js/faker": "8.3.1",
|
||||
"@faker-js/faker": "8.4.0",
|
||||
"@histoire/plugin-screenshot": "0.17.8",
|
||||
"@histoire/plugin-vue": "0.17.8",
|
||||
"@rushstack/eslint-patch": "1.6.1",
|
||||
"@histoire/plugin-vue": "0.17.9",
|
||||
"@rushstack/eslint-patch": "1.7.2",
|
||||
"@tsconfig/node18": "18.2.2",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
|
@ -136,43 +138,43 @@
|
|||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/marked": "5.0.2",
|
||||
"@types/node": "20.11.4",
|
||||
"@types/node": "20.11.10",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.19.0",
|
||||
"@typescript-eslint/parser": "6.19.0",
|
||||
"@vitejs/plugin-legacy": "5.2.0",
|
||||
"@vitejs/plugin-vue": "4.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.20.0",
|
||||
"@typescript-eslint/parser": "6.20.0",
|
||||
"@vitejs/plugin-legacy": "5.3.0",
|
||||
"@vitejs/plugin-vue": "5.0.3",
|
||||
"@vue/eslint-config-typescript": "12.0.0",
|
||||
"@vue/test-utils": "2.4.3",
|
||||
"@vue/test-utils": "2.4.4",
|
||||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.16",
|
||||
"browserslist": "4.22.2",
|
||||
"caniuse-lite": "1.0.30001577",
|
||||
"autoprefixer": "10.4.17",
|
||||
"browserslist": "4.22.3",
|
||||
"caniuse-lite": "1.0.30001581",
|
||||
"css-has-pseudo": "6.0.1",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.6.2",
|
||||
"esbuild": "0.19.11",
|
||||
"cypress": "13.6.3",
|
||||
"esbuild": "0.20.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-vue": "9.20.1",
|
||||
"happy-dom": "12.10.3",
|
||||
"histoire": "0.17.8",
|
||||
"happy-dom": "13.3.5",
|
||||
"histoire": "0.17.9",
|
||||
"postcss": "8.4.33",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-focus-within": "8.0.1",
|
||||
"postcss-preset-env": "9.3.0",
|
||||
"rollup": "4.9.5",
|
||||
"rollup": "4.9.6",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.69.7",
|
||||
"sass": "1.70.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.11",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.17.4",
|
||||
"vite-plugin-pwa": "0.17.5",
|
||||
"vite-plugin-sentry": "1.3.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.2.0",
|
||||
"vitest": "1.2.2",
|
||||
"vue-tsc": "1.8.27",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
|
|
3248
pnpm-lock.yaml
3248
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
34
src/App.vue
34
src/App.vue
|
@ -1,23 +1,23 @@
|
|||
<template>
|
||||
<ready>
|
||||
<Ready>
|
||||
<template v-if="authUser">
|
||||
<TheNavigation/>
|
||||
<content-auth/>
|
||||
<TheNavigation />
|
||||
<ContentAuth />
|
||||
</template>
|
||||
<content-link-share v-else-if="authLinkShare"/>
|
||||
<no-auth-wrapper v-else>
|
||||
<router-view/>
|
||||
</no-auth-wrapper>
|
||||
<ContentLinkShare v-else-if="authLinkShare" />
|
||||
<NoAuthWrapper v-else>
|
||||
<router-view />
|
||||
</NoAuthWrapper>
|
||||
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
<KeyboardShortcuts v-if="keyboardShortcutsActive" />
|
||||
|
||||
<Teleport to="body">
|
||||
<AddToHomeScreen/>
|
||||
<UpdateNotification/>
|
||||
<Notification/>
|
||||
<DemoMode/>
|
||||
<AddToHomeScreen />
|
||||
<UpdateNotification />
|
||||
<Notification />
|
||||
<DemoMode />
|
||||
</Teleport>
|
||||
</ready>
|
||||
</Ready>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -37,8 +37,6 @@ import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
|||
import Ready from '@/components/misc/ready.vue'
|
||||
|
||||
import {setLanguage} from '@/i18n'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
@ -48,6 +46,9 @@ import {useBodyClass} from '@/composables/useBodyClass'
|
|||
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
|
||||
import DemoMode from '@/components/home/DemoMode.vue'
|
||||
|
||||
const importAccountDeleteService = () => import('@/services/accountDelete')
|
||||
const importMessage = () => import('@/message')
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
@ -68,8 +69,11 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
|||
return
|
||||
}
|
||||
|
||||
const messageP = importMessage()
|
||||
const AccountDeleteService = (await importAccountDeleteService()).default
|
||||
const accountDeletionService = new AccountDeleteService()
|
||||
await accountDeletionService.confirm(accountDeletionConfirm)
|
||||
const {success} = await messageP
|
||||
success({message: t('user.deletion.confirmSuccess')})
|
||||
authStore.refreshUserInfo()
|
||||
}, { immediate: true })
|
||||
|
|
|
@ -21,10 +21,16 @@ const state = reactive({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Story :setup-app="setupApp" :layout="{ type: 'grid', width: '200px' }">
|
||||
<Story
|
||||
:setup-app="setupApp"
|
||||
:layout="{ type: 'grid', width: '200px' }"
|
||||
>
|
||||
<Variant title="custom">
|
||||
<template #controls>
|
||||
<HstCheckbox v-model="state.disabled" title="Disabled" />
|
||||
<HstCheckbox
|
||||
v-model="state.disabled"
|
||||
title="Disabled"
|
||||
/>
|
||||
</template>
|
||||
<BaseButton :disabled="state.disabled">
|
||||
Hello!
|
||||
|
|
|
@ -6,38 +6,39 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="disabled === true && (to !== undefined || href !== undefined)"
|
||||
ref="button"
|
||||
class="base-button"
|
||||
:aria-disabled="disabled || undefined"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
<router-link
|
||||
v-else-if="to !== undefined"
|
||||
ref="button"
|
||||
:to="to"
|
||||
class="base-button"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
<slot />
|
||||
</router-link>
|
||||
<a v-else-if="href !== undefined"
|
||||
<a
|
||||
v-else-if="href !== undefined"
|
||||
ref="button"
|
||||
class="base-button"
|
||||
:href="href"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
ref="button"
|
||||
>
|
||||
<slot/>
|
||||
<slot />
|
||||
</a>
|
||||
<button
|
||||
v-else
|
||||
ref="button"
|
||||
:type="type"
|
||||
class="base-button base-button--type-button"
|
||||
:disabled="disabled || undefined"
|
||||
ref="button"
|
||||
@click="(event: MouseEvent) => emit('click', event)"
|
||||
>
|
||||
<slot/>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
<template>
|
||||
<div class="base-checkbox" v-cy="'checkbox'">
|
||||
<div
|
||||
v-cy="'checkbox'"
|
||||
class="base-checkbox"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="checkboxId"
|
||||
type="checkbox"
|
||||
class="is-sr-only"
|
||||
:checked="modelValue"
|
||||
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
|
||||
:disabled="disabled || undefined"
|
||||
/>
|
||||
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
|
||||
>
|
||||
|
||||
<slot name="label" :checkboxId="checkboxId">
|
||||
<label :for="checkboxId" class="base-checkbox__label">
|
||||
<slot/>
|
||||
<slot
|
||||
name="label"
|
||||
:checkbox-id="checkboxId"
|
||||
>
|
||||
<label
|
||||
:for="checkboxId"
|
||||
class="base-checkbox__label"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
</slot>
|
||||
</div>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<transition
|
||||
name="expandable-slide"
|
||||
@before-enter="beforeEnter"
|
||||
@beforeEnter="beforeEnter"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@enter-cancelled="enterCancelled"
|
||||
@before-leave="beforeLeave"
|
||||
@afterEnter="afterEnter"
|
||||
@enterCancelled="enterCancelled"
|
||||
@beforeLeave="beforeLeave"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
@leave-cancelled="leaveCancelled"
|
||||
@afterLeave="afterLeave"
|
||||
@leaveCancelled="leaveCancelled"
|
||||
>
|
||||
<div
|
||||
v-if="initialHeight"
|
||||
|
@ -18,7 +18,10 @@
|
|||
>
|
||||
<slot />
|
||||
</div>
|
||||
<div v-else-if="open" class="expandable">
|
||||
<div
|
||||
v-else-if="open"
|
||||
class="expandable"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</transition>
|
||||
|
|
|
@ -7,21 +7,29 @@
|
|||
{{ $t('input.datemathHelp.intro') }}
|
||||
</p>
|
||||
<p>
|
||||
<i18n-t keypath="input.datemathHelp.expression" scope="global">
|
||||
<i18n-t
|
||||
keypath="input.datemathHelp.expression"
|
||||
scope="global"
|
||||
>
|
||||
<code>now</code>
|
||||
<code>||</code>
|
||||
</i18n-t>
|
||||
</p>
|
||||
<p>
|
||||
<i18n-t keypath="input.datemathHelp.similar" scope="global">
|
||||
<i18n-t
|
||||
keypath="input.datemathHelp.similar"
|
||||
scope="global"
|
||||
>
|
||||
<BaseButton
|
||||
href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/"
|
||||
target="_blank">
|
||||
target="_blank"
|
||||
>
|
||||
Grafana
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
href="https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math"
|
||||
target="_blank">
|
||||
target="_blank"
|
||||
>
|
||||
Elasticsearch
|
||||
</BaseButton>
|
||||
</i18n-t>
|
||||
|
@ -100,7 +108,10 @@
|
|||
<tr>
|
||||
<td><code>{{ exampleDate }}||+1M/d</code></td>
|
||||
<td>
|
||||
<i18n-t keypath="input.datemathHelp.examples.datePlusMonth" scope="global">
|
||||
<i18n-t
|
||||
keypath="input.datemathHelp.examples.datePlusMonth"
|
||||
scope="global"
|
||||
>
|
||||
<strong>{{ exampleDate }}</strong>
|
||||
</i18n-t>
|
||||
</td>
|
||||
|
|
|
@ -1,20 +1,31 @@
|
|||
<template>
|
||||
<div class="datepicker-with-range-container">
|
||||
<popup>
|
||||
<Popup>
|
||||
<template #trigger="{toggle}">
|
||||
<slot name="trigger" :toggle="toggle" :buttonText="buttonText"></slot>
|
||||
<slot
|
||||
name="trigger"
|
||||
:toggle="toggle"
|
||||
:button-text="buttonText"
|
||||
/>
|
||||
</template>
|
||||
<template #content="{isOpen}">
|
||||
<div class="datepicker-with-range" :class="{'is-open': isOpen}">
|
||||
<div
|
||||
class="datepicker-with-range"
|
||||
:class="{'is-open': isOpen}"
|
||||
>
|
||||
<div class="selections">
|
||||
<BaseButton @click="setDateRange(null)" :class="{'is-active': customRangeActive}">
|
||||
<BaseButton
|
||||
:class="{'is-active': customRangeActive}"
|
||||
@click="setDateRange(null)"
|
||||
>
|
||||
{{ $t('misc.custom') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-for="(value, text) in DATE_RANGES"
|
||||
:key="text"
|
||||
:class="{'is-active': from === value[0] && to === value[1]}"
|
||||
@click="setDateRange(value)"
|
||||
:class="{'is-active': from === value[0] && to === value[1]}">
|
||||
>
|
||||
{{ $t(`input.datepickerRange.ranges.${text}`) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
@ -23,10 +34,18 @@
|
|||
{{ $t('input.datepickerRange.from') }}
|
||||
<div class="field has-addons">
|
||||
<div class="control is-fullwidth">
|
||||
<input class="input" type="text" v-model="from"/>
|
||||
<input
|
||||
v-model="from"
|
||||
class="input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button icon="calendar" variant="secondary" data-toggle/>
|
||||
<x-button
|
||||
icon="calendar"
|
||||
variant="secondary"
|
||||
data-toggle
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
@ -34,38 +53,49 @@
|
|||
{{ $t('input.datepickerRange.to') }}
|
||||
<div class="field has-addons">
|
||||
<div class="control is-fullwidth">
|
||||
<input class="input" type="text" v-model="to"/>
|
||||
<input
|
||||
v-model="to"
|
||||
class="input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button icon="calendar" variant="secondary" data-toggle/>
|
||||
<x-button
|
||||
icon="calendar"
|
||||
variant="secondary"
|
||||
data-toggle
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<flat-pickr
|
||||
:config="flatPickerConfig"
|
||||
v-model="flatpickrRange"
|
||||
:config="flatPickerConfig"
|
||||
/>
|
||||
|
||||
<p>
|
||||
{{ $t('input.datemathHelp.canuse') }}
|
||||
<BaseButton class="has-text-primary" @click="showHowItWorks = true">
|
||||
<BaseButton
|
||||
class="has-text-primary"
|
||||
@click="showHowItWorks = true"
|
||||
>
|
||||
{{ $t('input.datemathHelp.learnhow') }}
|
||||
</BaseButton>
|
||||
</p>
|
||||
|
||||
<modal
|
||||
:enabled="showHowItWorks"
|
||||
@close="() => showHowItWorks = false"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
@close="() => showHowItWorks = false"
|
||||
>
|
||||
<DatemathHelp/>
|
||||
<DatemathHelp />
|
||||
</modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</popup>
|
||||
</Popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -81,20 +111,18 @@ import Popup from '@/components/misc/popup.vue'
|
|||
import {DATE_RANGES} from '@/components/date/dateRanges'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/datemathHelp.vue'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
// FIXME: This seems to always contain the default value - that breaks the picker
|
||||
const weekStart = computed(() => authStore.settings.weekStart ?? 0)
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
|
@ -102,9 +130,7 @@ const flatPickerConfig = computed(() => ({
|
|||
enableTime: false,
|
||||
wrap: true,
|
||||
mode: 'range',
|
||||
locale: {
|
||||
firstDayOf7Days: weekStart.value,
|
||||
},
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
|
||||
const showHowItWorks = ref(false)
|
||||
|
|
|
@ -4,12 +4,18 @@
|
|||
class="add-to-home-screen"
|
||||
:class="{'has-update-available': hasUpdateAvailable}"
|
||||
>
|
||||
<icon icon="arrow-up-from-bracket" class="add-icon"/>
|
||||
<icon
|
||||
icon="arrow-up-from-bracket"
|
||||
class="add-icon"
|
||||
/>
|
||||
<p>
|
||||
{{ $t('home.addToHomeScreen') }}
|
||||
</p>
|
||||
<BaseButton @click="() => hideMessage = true" class="hide-button">
|
||||
<icon icon="x"/>
|
||||
<BaseButton
|
||||
class="hide-button"
|
||||
@click="() => hideMessage = true"
|
||||
>
|
||||
<icon icon="x" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -17,8 +17,11 @@ const enabled = computed(() => configStore.demoModeEnabled && !hide.value)
|
|||
{{ $t('demo.title') }}
|
||||
<strong class="is-uppercase">{{ $t('demo.everythingWillBeDeleted') }}</strong>
|
||||
</p>
|
||||
<BaseButton @click="() => hide = true" class="hide-button">
|
||||
<icon icon="times"/>
|
||||
<BaseButton
|
||||
class="hide-button"
|
||||
@click="() => hide = true"
|
||||
>
|
||||
<icon icon="times" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -15,8 +15,17 @@ const CustomLogo = computed(() => window.CUSTOM_LOGO_URL)
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<Logo v-if="!CustomLogo" alt="Vikunja" class="logo" />
|
||||
<img v-show="CustomLogo" :src="CustomLogo" alt="Vikunja" class="logo" />
|
||||
<Logo
|
||||
v-if="!CustomLogo"
|
||||
alt="Vikunja"
|
||||
class="logo"
|
||||
/>
|
||||
<img
|
||||
v-show="CustomLogo"
|
||||
:src="CustomLogo"
|
||||
alt="Vikunja"
|
||||
class="logo"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<BaseButton
|
||||
class="menu-show-button"
|
||||
@click="baseStore.toggleMenu()"
|
||||
@shortkey="() => baseStore.toggleMenu()"
|
||||
v-shortcut="'Mod+e'"
|
||||
class="menu-show-button"
|
||||
:title="$t('keyboardShortcuts.toggleMenu')"
|
||||
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
|
||||
@click="baseStore.toggleMenu()"
|
||||
@shortkey="() => baseStore.toggleMenu()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<BaseButton class="menu-bottom-link" :href="poweredByUrl" target="_blank">
|
||||
<BaseButton
|
||||
class="menu-bottom-link"
|
||||
:href="poweredByUrl"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('misc.poweredBy') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
<draggable
|
||||
v-model="availableProjects"
|
||||
animation="100"
|
||||
ghostClass="ghost"
|
||||
ghost-class="ghost"
|
||||
group="projects"
|
||||
@start="() => drag = true"
|
||||
@end="saveProjectPosition"
|
||||
handle=".handle"
|
||||
tag="menu"
|
||||
item-key="id"
|
||||
|
@ -19,6 +17,8 @@
|
|||
{ 'dragging-disabled': !canEditOrder }
|
||||
],
|
||||
}"
|
||||
@start="() => drag = true"
|
||||
@end="saveProjectPosition"
|
||||
>
|
||||
<template #item="{element: project}">
|
||||
<ProjectsNavigationItem
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
<div>
|
||||
<BaseButton
|
||||
v-if="canCollapse && childProjects?.length > 0"
|
||||
@click="childProjectsOpen = !childProjectsOpen"
|
||||
class="collapse-project-button"
|
||||
@click="childProjectsOpen = !childProjectsOpen"
|
||||
>
|
||||
<icon icon="chevron-down" :class="{ 'project-is-collapsed': !childProjectsOpen }"/>
|
||||
<icon
|
||||
icon="chevron-down"
|
||||
:class="{ 'project-is-collapsed': !childProjectsOpen }"
|
||||
/>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:to="{ name: 'project.index', params: { projectId: project.id} }"
|
||||
|
@ -19,21 +22,27 @@
|
|||
<span
|
||||
v-if="!canCollapse || childProjects?.length === 0"
|
||||
class="collapse-project-button-placeholder"
|
||||
></span>
|
||||
<div class="color-bubble-handle-wrapper" :class="{'is-draggable': project.id > 0}">
|
||||
/>
|
||||
<div
|
||||
class="color-bubble-handle-wrapper"
|
||||
:class="{'is-draggable': project.id > 0}"
|
||||
>
|
||||
<ColorBubble
|
||||
v-if="project.hexColor !== ''"
|
||||
:color="project.hexColor"
|
||||
/>
|
||||
<span v-else-if="project.id < -1" class="saved-filter-icon icon menu-item-icon">
|
||||
<icon icon="filter"/>
|
||||
<span
|
||||
v-else-if="project.id < -1"
|
||||
class="saved-filter-icon icon menu-item-icon"
|
||||
>
|
||||
<icon icon="filter" />
|
||||
</span>
|
||||
<span
|
||||
v-if="project.id > 0"
|
||||
class="icon menu-item-icon handle lines-handle"
|
||||
class="icon menu-item-icon handle"
|
||||
:class="{'has-color-bubble': project.hexColor !== ''}"
|
||||
>
|
||||
<icon icon="grip-lines"/>
|
||||
<icon icon="grip-lines" />
|
||||
</span>
|
||||
</div>
|
||||
<span class="project-menu-title">{{ getProjectTitle(project) }}</span>
|
||||
|
@ -44,7 +53,7 @@
|
|||
:class="{'is-favorite': project.isFavorite}"
|
||||
@click="projectStore.toggleProjectFavorite(project)"
|
||||
>
|
||||
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']"/>
|
||||
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']" />
|
||||
</BaseButton>
|
||||
<ProjectSettingsDropdown
|
||||
class="menu-list-dropdown"
|
||||
|
@ -52,8 +61,14 @@
|
|||
:level="level"
|
||||
>
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
<BaseButton
|
||||
class="menu-list-dropdown-trigger"
|
||||
@click="toggleOpen"
|
||||
>
|
||||
<icon
|
||||
icon="ellipsis-h"
|
||||
class="icon"
|
||||
/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</ProjectSettingsDropdown>
|
||||
|
|
|
@ -1,66 +1,110 @@
|
|||
<template>
|
||||
<header :class="{ 'has-background': background, 'menu-active': menuActive }" aria-label="main navigation"
|
||||
class="navbar d-print-none">
|
||||
<router-link :to="{ name: 'home' }" class="logo-link">
|
||||
<Logo width="164" height="48" />
|
||||
<header
|
||||
:class="{ 'has-background': background, 'menu-active': menuActive }"
|
||||
aria-label="main navigation"
|
||||
class="navbar d-print-none"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'home' }"
|
||||
class="logo-link"
|
||||
>
|
||||
<Logo
|
||||
width="164"
|
||||
height="48"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
<MenuButton class="menu-button" />
|
||||
|
||||
<div v-if="currentProject?.id" class="project-title-wrapper">
|
||||
<div
|
||||
v-if="currentProject?.id"
|
||||
class="project-title-wrapper"
|
||||
>
|
||||
<h1 class="project-title">
|
||||
{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
|
||||
</h1>
|
||||
|
||||
<BaseButton :to="{ name: 'project.info', params: { projectId: currentProject.id } }" class="project-title-button">
|
||||
<BaseButton
|
||||
:to="{ name: 'project.info', params: { projectId: currentProject.id } }"
|
||||
class="project-title-button"
|
||||
>
|
||||
<icon icon="circle-info" />
|
||||
</BaseButton>
|
||||
|
||||
<project-settings-dropdown v-if="canWriteCurrentProject && currentProject.id !== -1"
|
||||
class="project-title-dropdown" :project="currentProject">
|
||||
<ProjectSettingsDropdown
|
||||
v-if="canWriteCurrentProject && currentProject.id !== -1"
|
||||
class="project-title-dropdown"
|
||||
:project="currentProject"
|
||||
>
|
||||
<template #trigger="{ toggleOpen }">
|
||||
<BaseButton class="project-title-button" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon" />
|
||||
<BaseButton
|
||||
class="project-title-button"
|
||||
@click="toggleOpen"
|
||||
>
|
||||
<icon
|
||||
icon="ellipsis-h"
|
||||
class="icon"
|
||||
/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</project-settings-dropdown>
|
||||
</ProjectSettingsDropdown>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<OpenQuickActions/>
|
||||
<OpenQuickActions />
|
||||
<Notifications />
|
||||
<dropdown>
|
||||
<Dropdown>
|
||||
<template #trigger="{ toggleOpen, open }">
|
||||
<BaseButton class="username-dropdown-trigger" @click="toggleOpen" variant="secondary" :shadow="false">
|
||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40" />
|
||||
<BaseButton
|
||||
class="username-dropdown-trigger"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
@click="toggleOpen"
|
||||
>
|
||||
<img
|
||||
:src="authStore.avatarUrl"
|
||||
alt=""
|
||||
class="avatar"
|
||||
width="40"
|
||||
height="40"
|
||||
>
|
||||
<span class="username">{{ authStore.userDisplayName }}</span>
|
||||
<span class="icon is-small" :style="{
|
||||
<span
|
||||
class="icon is-small"
|
||||
:style="{
|
||||
transform: open ? 'rotate(180deg)' : 'rotate(0)',
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<icon icon="chevron-down" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<dropdown-item :to="{ name: 'user.settings' }">
|
||||
<DropdownItem :to="{ name: 'user.settings' }">
|
||||
{{ $t('user.settings.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item v-if="imprintUrl" :href="imprintUrl">
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-if="imprintUrl"
|
||||
:href="imprintUrl"
|
||||
>
|
||||
{{ $t('navigation.imprint') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item v-if="privacyPolicyUrl" :href="privacyPolicyUrl">
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-if="privacyPolicyUrl"
|
||||
:href="privacyPolicyUrl"
|
||||
>
|
||||
{{ $t('navigation.privacy') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
|
||||
</DropdownItem>
|
||||
<DropdownItem @click="baseStore.setKeyboardShortcutsActive(true)">
|
||||
{{ $t('keyboardShortcuts.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item :to="{ name: 'about' }">
|
||||
</DropdownItem>
|
||||
<DropdownItem :to="{ name: 'about' }">
|
||||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item @click="authStore.logout()">
|
||||
</DropdownItem>
|
||||
<DropdownItem @click="authStore.logout()">
|
||||
{{ $t('user.auth.logout') }}
|
||||
</dropdown-item>
|
||||
</dropdown>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
<template>
|
||||
<div class="update-notification" v-if="updateAvailable">
|
||||
<p class="update-notification__message">{{ $t('update.available') }}</p>
|
||||
<div
|
||||
v-if="updateAvailable"
|
||||
class="update-notification"
|
||||
>
|
||||
<p class="update-notification__message">
|
||||
{{ $t('update.available') }}
|
||||
</p>
|
||||
<x-button
|
||||
@click="refreshApp()"
|
||||
:shadow="false"
|
||||
:wrap="false"
|
||||
@click="refreshApp()"
|
||||
>
|
||||
{{ $t('update.do') }}
|
||||
</x-button>
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
<div class="content-auth">
|
||||
<BaseButton
|
||||
v-show="menuActive"
|
||||
@click="baseStore.setMenuActive(false)"
|
||||
class="menu-hide-button d-print-none"
|
||||
@click="baseStore.setMenuActive(false)"
|
||||
>
|
||||
<icon icon="times"/>
|
||||
<icon icon="times" />
|
||||
</BaseButton>
|
||||
<div
|
||||
class="app-container"
|
||||
|
@ -15,8 +15,9 @@
|
|||
<div
|
||||
:class="{'is-visible': background}"
|
||||
class="app-container-background background-fade-in d-print-none"
|
||||
:style="{'background-image': background && `url(${background})`}"></div>
|
||||
<navigation class="d-print-none"/>
|
||||
:style="{'background-image': background && `url(${background})`}"
|
||||
/>
|
||||
<Navigation class="d-print-none" />
|
||||
<main
|
||||
class="app-content"
|
||||
:class="[
|
||||
|
@ -26,33 +27,36 @@
|
|||
>
|
||||
<BaseButton
|
||||
v-show="menuActive"
|
||||
@click="baseStore.setMenuActive(false)"
|
||||
class="mobile-overlay d-print-none"
|
||||
@click="baseStore.setMenuActive(false)"
|
||||
/>
|
||||
|
||||
<quick-actions/>
|
||||
<QuickActions />
|
||||
|
||||
<router-view :route="routeWithModal" v-slot="{ Component }">
|
||||
<router-view
|
||||
v-slot="{ Component }"
|
||||
:route="routeWithModal"
|
||||
>
|
||||
<keep-alive :include="['project.list', 'project.gantt', 'project.table', 'project.kanban']">
|
||||
<component :is="Component"/>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
<modal
|
||||
:enabled="typeof currentModal !== 'undefined'"
|
||||
@close="closeModal()"
|
||||
variant="scrolling"
|
||||
class="task-detail-view-modal"
|
||||
@close="closeModal()"
|
||||
>
|
||||
<component :is="currentModal"/>
|
||||
<component :is="currentModal" />
|
||||
</modal>
|
||||
|
||||
<BaseButton
|
||||
v-shortcut="'?'"
|
||||
class="keyboard-shortcuts-button d-print-none"
|
||||
@click="showKeyboardShortcuts()"
|
||||
v-shortcut="'?'"
|
||||
>
|
||||
<icon icon="keyboard"/>
|
||||
<icon icon="keyboard" />
|
||||
</BaseButton>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -6,16 +6,20 @@
|
|||
>
|
||||
<div class="container has-text-centered link-share-view">
|
||||
<div class="column is-10 is-offset-1">
|
||||
<Logo class="logo" v-if="logoVisible"/>
|
||||
<Logo
|
||||
v-if="logoVisible"
|
||||
class="logo"
|
||||
/>
|
||||
<h1
|
||||
:class="{'m-0': !logoVisible}"
|
||||
:style="{ 'opacity': currentProject?.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
class="title"
|
||||
>
|
||||
{{ currentProject?.title === '' ? $t('misc.loading') : currentProject?.title }}
|
||||
</h1>
|
||||
<div class="box has-text-left view">
|
||||
<router-view/>
|
||||
<PoweredByLink/>
|
||||
<router-view />
|
||||
<PoweredByLink />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,46 +1,70 @@
|
|||
<template>
|
||||
<aside :class="{'is-active': baseStore.menuActive}" class="menu-container">
|
||||
<aside
|
||||
:class="{'is-active': baseStore.menuActive}"
|
||||
class="menu-container"
|
||||
>
|
||||
<nav class="menu top-menu">
|
||||
<router-link :to="{name: 'home'}" class="logo">
|
||||
<Logo width="164" height="48"/>
|
||||
<router-link
|
||||
:to="{name: 'home'}"
|
||||
class="logo"
|
||||
>
|
||||
<Logo
|
||||
width="164"
|
||||
height="48"
|
||||
/>
|
||||
</router-link>
|
||||
<menu class="menu-list other-menu-items">
|
||||
<li>
|
||||
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
|
||||
<router-link
|
||||
v-shortcut="'g o'"
|
||||
:to="{ name: 'home'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
<icon icon="calendar"/>
|
||||
<icon icon="calendar" />
|
||||
</span>
|
||||
{{ $t('navigation.overview') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'tasks.range'}" v-shortcut="'g u'">
|
||||
<router-link
|
||||
v-shortcut="'g u'"
|
||||
:to="{ name: 'tasks.range'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
<icon :icon="['far', 'calendar-alt']" />
|
||||
</span>
|
||||
{{ $t('navigation.upcoming') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'projects.index'}" v-shortcut="'g p'">
|
||||
<router-link
|
||||
v-shortcut="'g p'"
|
||||
:to="{ name: 'projects.index'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
<icon icon="layer-group"/>
|
||||
<icon icon="layer-group" />
|
||||
</span>
|
||||
{{ $t('project.projects') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'labels.index'}" v-shortcut="'g a'">
|
||||
<router-link
|
||||
v-shortcut="'g a'"
|
||||
:to="{ name: 'labels.index'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
<icon icon="tags"/>
|
||||
<icon icon="tags" />
|
||||
</span>
|
||||
{{ $t('label.title') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'teams.index'}" v-shortcut="'g m'">
|
||||
<router-link
|
||||
v-shortcut="'g m'"
|
||||
:to="{ name: 'teams.index'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
<icon icon="users"/>
|
||||
<icon icon="users" />
|
||||
</span>
|
||||
{{ $t('team.title') }}
|
||||
</router-link>
|
||||
|
@ -53,7 +77,10 @@
|
|||
variant="small"
|
||||
/>
|
||||
<template v-else>
|
||||
<nav class="menu" v-if="favoriteProjects">
|
||||
<nav
|
||||
v-if="favoriteProjects"
|
||||
class="menu"
|
||||
>
|
||||
<ProjectsNavigation
|
||||
:model-value="favoriteProjects"
|
||||
:can-edit-order="false"
|
||||
|
@ -61,7 +88,10 @@
|
|||
/>
|
||||
</nav>
|
||||
|
||||
<nav class="menu" v-if="savedFilterProjects">
|
||||
<nav
|
||||
v-if="savedFilterProjects"
|
||||
class="menu"
|
||||
>
|
||||
<ProjectsNavigation
|
||||
:model-value="savedFilterProjects"
|
||||
:can-edit-order="false"
|
||||
|
@ -79,7 +109,7 @@
|
|||
</nav>
|
||||
</template>
|
||||
|
||||
<PoweredByLink/>
|
||||
<PoweredByLink />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -6,19 +6,28 @@ import XButton from './button.vue'
|
|||
<template>
|
||||
<Story :layout="{ type: 'grid', width: '200px' }">
|
||||
<Variant title="primary">
|
||||
<XButton @click="logEvent('Click', $event)" variant="primary">
|
||||
<XButton
|
||||
variant="primary"
|
||||
@click="logEvent('Click', $event)"
|
||||
>
|
||||
Order pizza!
|
||||
</XButton>
|
||||
</Variant>
|
||||
|
||||
<Variant title="secondary">
|
||||
<XButton @click="logEvent('Click', $event)" variant="secondary">
|
||||
<XButton
|
||||
variant="secondary"
|
||||
@click="logEvent('Click', $event)"
|
||||
>
|
||||
Order spaghetti!
|
||||
</XButton>
|
||||
</Variant>
|
||||
|
||||
<Variant title="tertiary">
|
||||
<XButton @click="logEvent('Click', $event)" variant="tertiary">
|
||||
<XButton
|
||||
variant="tertiary"
|
||||
@click="logEvent('Click', $event)"
|
||||
>
|
||||
Order tortellini!
|
||||
</XButton>
|
||||
</Variant>
|
||||
|
|
|
@ -1,36 +1,67 @@
|
|||
<template>
|
||||
<div class="color-picker-container">
|
||||
<datalist :id="colorListID">
|
||||
<option v-for="defaultColor in defaultColors" :key="defaultColor" :value="defaultColor" />
|
||||
<option
|
||||
v-for="defaultColor in defaultColors"
|
||||
:key="defaultColor"
|
||||
:value="defaultColor"
|
||||
/>
|
||||
</datalist>
|
||||
|
||||
<div class="picker">
|
||||
<input
|
||||
v-model="color"
|
||||
class="picker__input"
|
||||
type="color"
|
||||
v-model="color"
|
||||
:list="colorListID"
|
||||
:class="{'is-empty': isEmpty}"
|
||||
>
|
||||
<svg
|
||||
v-show="isEmpty"
|
||||
class="picker__pattern"
|
||||
viewBox="0 0 22 22"
|
||||
fill="fff"
|
||||
>
|
||||
<pattern
|
||||
id="checker"
|
||||
width="11"
|
||||
height="11"
|
||||
patternUnits="userSpaceOnUse"
|
||||
fill="FFF"
|
||||
>
|
||||
<rect
|
||||
fill="#cccccc"
|
||||
x="0"
|
||||
width="5.5"
|
||||
height="5.5"
|
||||
y="0"
|
||||
/>
|
||||
<rect
|
||||
fill="#cccccc"
|
||||
x="5.5"
|
||||
width="5.5"
|
||||
height="5.5"
|
||||
y="5.5"
|
||||
/>
|
||||
<svg class="picker__pattern" v-show="isEmpty" viewBox="0 0 22 22" fill="fff">
|
||||
<pattern id="checker" width="11" height="11" patternUnits="userSpaceOnUse" fill="FFF">
|
||||
<rect fill="#cccccc" x="0" width="5.5" height="5.5" y="0"></rect>
|
||||
<rect fill="#cccccc" x="5.5" width="5.5" height="5.5" y="5.5"></rect>
|
||||
</pattern>
|
||||
<rect width="22" height="22" fill="url(#checker)"></rect>
|
||||
<rect
|
||||
width="22"
|
||||
height="22"
|
||||
fill="url(#checker)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<x-button
|
||||
<XButton
|
||||
v-if="!isEmpty"
|
||||
:disabled="isEmpty"
|
||||
@click="reset"
|
||||
class="is-small ml-2"
|
||||
:shadow="false"
|
||||
variant="secondary"
|
||||
@click="reset"
|
||||
>
|
||||
{{ $t('input.resetColor') }}
|
||||
</x-button>
|
||||
</XButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -39,6 +70,14 @@ import {computed, ref, watch} from 'vue'
|
|||
import {createRandomID} from '@/helpers/randomId'
|
||||
import XButton from '@/components/input/button.vue'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const DEFAULT_COLORS = [
|
||||
'#1973ff',
|
||||
'#7F23FF',
|
||||
|
@ -53,17 +92,18 @@ const lastChangeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
|||
const defaultColors = ref(DEFAULT_COLORS)
|
||||
const colorListID = ref(createRandomID())
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
(newValue) => {
|
||||
if (newValue === '' || newValue.startsWith('var(')) {
|
||||
color.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (!newValue.startsWith('#') && (newValue.length === 6 || newValue.length === 3)) {
|
||||
newValue = `#${newValue}`
|
||||
}
|
||||
|
||||
color.value = newValue
|
||||
},
|
||||
{immediate: true},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<multiselect
|
||||
<Multiselect
|
||||
v-model="selectedProjects"
|
||||
:search-results="foundProjects"
|
||||
:loading="projectService.loading"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<multiselect
|
||||
<Multiselect
|
||||
v-model="selectedUsers"
|
||||
:search-results="foundUsers"
|
||||
:loading="userService.loading"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<BaseButton class="simple-button">
|
||||
<slot/>
|
||||
<slot />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -18,7 +18,10 @@
|
|||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : undefined}"
|
||||
/>
|
||||
<span class="icon is-small" v-else>
|
||||
<span
|
||||
v-else
|
||||
class="icon is-small"
|
||||
>
|
||||
<icon
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : undefined}"
|
||||
|
@ -38,7 +41,7 @@ const BUTTON_TYPES_MAP = {
|
|||
|
||||
export type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
|
||||
|
||||
export default { name: 'x-button' }
|
||||
export default { name: 'XButton' }
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
<template>
|
||||
<div class="datepicker">
|
||||
<SimpleButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
|
||||
<SimpleButton
|
||||
class="show"
|
||||
:disabled="disabled || undefined"
|
||||
@click.stop="toggleDatePopup"
|
||||
>
|
||||
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
|
||||
</SimpleButton>
|
||||
|
||||
<CustomTransition name="fade">
|
||||
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
|
||||
|
||||
<div
|
||||
v-if="show"
|
||||
ref="datepickerPopup"
|
||||
class="datepicker-popup"
|
||||
>
|
||||
<DatepickerInline
|
||||
v-model="date"
|
||||
@update:model-value="updateData"
|
||||
@update:modelValue="updateData"
|
||||
/>
|
||||
|
||||
<x-button
|
||||
v-cy="'closeDatepicker'"
|
||||
class="datepicker__close-button"
|
||||
:shadow="false"
|
||||
@click="close"
|
||||
v-cy="'closeDatepicker'"
|
||||
>
|
||||
{{ $t('misc.confirm') }}
|
||||
</x-button>
|
||||
|
@ -56,7 +63,7 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'closeOnChange'])
|
||||
|
||||
const date = ref<Date | null>()
|
||||
const show = ref(false)
|
||||
|
@ -108,7 +115,7 @@ function close() {
|
|||
emit('close', changed.value)
|
||||
if (changed.value) {
|
||||
changed.value = false
|
||||
emit('close-on-change', changed.value)
|
||||
emit('closeOnChange', changed.value)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('today')"
|
||||
>
|
||||
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
|
||||
<span class="icon"><icon :icon="['far', 'calendar-alt']" /></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.today') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
|
||||
|
@ -14,7 +14,7 @@
|
|||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('tomorrow')"
|
||||
>
|
||||
<span class="icon"><icon :icon="['far', 'sun']"/></span>
|
||||
<span class="icon"><icon :icon="['far', 'sun']" /></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.tomorrow') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
|
||||
|
@ -24,7 +24,7 @@
|
|||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('nextMonday')"
|
||||
>
|
||||
<span class="icon"><icon icon="coffee"/></span>
|
||||
<span class="icon"><icon icon="coffee" /></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.nextMonday') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
|
||||
|
@ -34,7 +34,7 @@
|
|||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('thisWeekend')"
|
||||
>
|
||||
<span class="icon"><icon icon="cocktail"/></span>
|
||||
<span class="icon"><icon icon="cocktail" /></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.thisWeekend') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
|
||||
|
@ -44,7 +44,7 @@
|
|||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('laterThisWeek')"
|
||||
>
|
||||
<span class="icon"><icon icon="chess-knight"/></span>
|
||||
<span class="icon"><icon icon="chess-knight" /></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.laterThisWeek') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
|
||||
|
@ -54,7 +54,7 @@
|
|||
class="datepicker__quick-select-date"
|
||||
@click.stop="setDate('nextWeek')"
|
||||
>
|
||||
<span class="icon"><icon icon="forward"/></span>
|
||||
<span class="icon"><icon icon="forward" /></span>
|
||||
<span class="text">
|
||||
<span>{{ $t('input.datepicker.nextWeek') }}</span>
|
||||
<span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
|
||||
|
@ -63,8 +63,8 @@
|
|||
|
||||
<div class="flatpickr-container">
|
||||
<flat-pickr
|
||||
:config="flatPickerConfig"
|
||||
v-model="flatPickrDate"
|
||||
:config="flatPickerConfig"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -80,8 +80,8 @@ import {formatDate} from '@/helpers/time/formatDate'
|
|||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -105,8 +105,6 @@ watch(
|
|||
{immediate: true},
|
||||
)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const weekStart = computed(() => authStore.settings.weekStart)
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
|
@ -114,9 +112,7 @@ const flatPickerConfig = computed(() => ({
|
|||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
locale: {
|
||||
firstDayOfWeek: weekStart.value,
|
||||
},
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
|
||||
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
||||
|
|
|
@ -2,26 +2,30 @@
|
|||
<div class="items">
|
||||
<template v-if="items.length">
|
||||
<button
|
||||
class="item"
|
||||
:class="{ 'is-selected': index === selectedIndex }"
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="item"
|
||||
:class="{ 'is-selected': index === selectedIndex }"
|
||||
@click="selectItem(index)"
|
||||
>
|
||||
<icon :icon="item.icon"/>
|
||||
<icon :icon="item.icon" />
|
||||
<div class="description">
|
||||
<p>{{ item.title }}</p>
|
||||
<p>{{ item.description }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
<div class="item" v-else>
|
||||
<div
|
||||
v-else
|
||||
class="item"
|
||||
>
|
||||
No result
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
/* eslint-disable vue/component-api-style */
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
|
|
|
@ -2,35 +2,35 @@
|
|||
<div class="editor-toolbar">
|
||||
<div class="editor-toolbar__segment">
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
||||
v-tooltip="$t('input.editor.heading1')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-header']"/>
|
||||
<icon :icon="['fa', 'fa-header']" />
|
||||
<span class="icon__lower-text">1</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
||||
v-tooltip="$t('input.editor.heading2')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-header']"/>
|
||||
<icon :icon="['fa', 'fa-header']" />
|
||||
<span class="icon__lower-text">2</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
||||
v-tooltip="$t('input.editor.heading3')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-header']"/>
|
||||
<icon :icon="['fa', 'fa-header']" />
|
||||
<span class="icon__lower-text">3</span>
|
||||
</span>
|
||||
</BaseButton>
|
||||
|
@ -38,167 +38,167 @@
|
|||
|
||||
<div class="editor-toolbar__segment">
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
v-tooltip="$t('input.editor.bold')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-bold']"/>
|
||||
<icon :icon="['fa', 'fa-bold']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
v-tooltip="$t('input.editor.italic')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-italic']"/>
|
||||
<icon :icon="['fa', 'fa-italic']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleUnderline().run()"
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
v-tooltip="$t('input.editor.underline')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
@click="editor.chain().focus().toggleUnderline().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-underline']"/>
|
||||
<icon :icon="['fa', 'fa-underline']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
v-tooltip="$t('input.editor.strikethrough')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-strikethrough']"/>
|
||||
<icon :icon="['fa', 'fa-strikethrough']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar__segment">
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleCodeBlock().run()"
|
||||
:class="{ 'is-active': editor.isActive('codeBlock') }"
|
||||
v-tooltip="$t('input.editor.code')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('codeBlock') }"
|
||||
@click="editor.chain().focus().toggleCodeBlock().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-code']"/>
|
||||
<icon :icon="['fa', 'fa-code']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||||
v-tooltip="$t('input.editor.quote')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-quote-right']"/>
|
||||
<icon :icon="['fa', 'fa-quote-right']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar__segment">
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||
v-tooltip="$t('input.editor.bulletList')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-list-ul']"/>
|
||||
<icon :icon="['fa', 'fa-list-ul']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||||
v-tooltip="$t('input.editor.orderedList')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-list-ol']"/>
|
||||
<icon :icon="['fa', 'fa-list-ol']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleTaskList().run()"
|
||||
:class="{ 'is-active': editor.isActive('taskList') }"
|
||||
v-tooltip="$t('input.editor.taskList')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('taskList') }"
|
||||
@click="editor.chain().focus().toggleTaskList().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon icon="fa-list-check"/>
|
||||
<icon icon="fa-list-check" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar__segment">
|
||||
<BaseButton
|
||||
v-tooltip="$t('input.editor.image')"
|
||||
class="editor-toolbar__button"
|
||||
@click="openImagePicker"
|
||||
v-tooltip="$t('input.editor.image')"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon icon="fa-image"/>
|
||||
<icon icon="fa-image" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar__segment">
|
||||
<BaseButton
|
||||
v-tooltip="$t('input.editor.link')"
|
||||
class="editor-toolbar__button"
|
||||
@click="setLink"
|
||||
:class="{ 'is-active': editor.isActive('link') }"
|
||||
title="set link"
|
||||
v-tooltip="$t('input.editor.link')"
|
||||
@click="setLink"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-link']"/>
|
||||
<icon :icon="['fa', 'fa-link']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-tooltip="$t('input.editor.text')"
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().setParagraph().run()"
|
||||
:class="{ 'is-active': editor.isActive('paragraph') }"
|
||||
title="paragraph"
|
||||
v-tooltip="$t('input.editor.text')"
|
||||
@click="editor.chain().focus().setParagraph().run()"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-paragraph']"/>
|
||||
<icon :icon="['fa', 'fa-paragraph']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
v-tooltip="$t('input.editor.horizontalRule')"
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().setHorizontalRule().run()"
|
||||
v-tooltip="$t('input.editor.horizontalRule')"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-ruler-horizontal']"/>
|
||||
<icon :icon="['fa', 'fa-ruler-horizontal']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="editor-toolbar__segment">
|
||||
<BaseButton
|
||||
v-tooltip="$t('input.editor.undo')"
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().undo().run()"
|
||||
v-tooltip="$t('input.editor.undo')"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-undo']"/>
|
||||
<icon :icon="['fa', 'fa-undo']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-tooltip="$t('input.editor.redo')"
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().redo().run()"
|
||||
v-tooltip="$t('input.editor.redo')"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-redo']"/>
|
||||
<icon :icon="['fa', 'fa-redo']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
@ -206,16 +206,19 @@
|
|||
<div class="editor-toolbar__segment">
|
||||
<!-- table -->
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="toggleTableMode"
|
||||
:class="{ 'is-active': editor.isActive('table') }"
|
||||
v-tooltip="$t('input.editor.table.title')"
|
||||
class="editor-toolbar__button"
|
||||
:class="{ 'is-active': editor.isActive('table') }"
|
||||
@click="toggleTableMode"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['fa', 'fa-table']"/>
|
||||
<icon :icon="['fa', 'fa-table']" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
<div v-if="tableMode" class="editor-toolbar__table-buttons">
|
||||
<div
|
||||
v-if="tableMode"
|
||||
class="editor-toolbar__table-buttons"
|
||||
>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="
|
||||
|
@ -230,99 +233,99 @@
|
|||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().addColumnBefore().run()"
|
||||
:disabled="!editor.can().addColumnBefore"
|
||||
@click="editor.chain().focus().addColumnBefore().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.addColumnBefore') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().addColumnAfter().run()"
|
||||
:disabled="!editor.can().addColumnAfter"
|
||||
@click="editor.chain().focus().addColumnAfter().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.addColumnAfter') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().deleteColumn().run()"
|
||||
:disabled="!editor.can().deleteColumn"
|
||||
@click="editor.chain().focus().deleteColumn().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.deleteColumn') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().addRowBefore().run()"
|
||||
:disabled="!editor.can().addRowBefore"
|
||||
@click="editor.chain().focus().addRowBefore().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.addRowBefore') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().addRowAfter().run()"
|
||||
:disabled="!editor.can().addRowAfter"
|
||||
@click="editor.chain().focus().addRowAfter().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.addRowAfter') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().deleteRow().run()"
|
||||
:disabled="!editor.can().deleteRow"
|
||||
@click="editor.chain().focus().deleteRow().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.deleteRow') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().deleteTable().run()"
|
||||
:disabled="!editor.can().deleteTable"
|
||||
@click="editor.chain().focus().deleteTable().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.deleteTable') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().mergeCells().run()"
|
||||
:disabled="!editor.can().mergeCells"
|
||||
@click="editor.chain().focus().mergeCells().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.mergeCells') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().splitCell().run()"
|
||||
:disabled="!editor.can().splitCell"
|
||||
@click="editor.chain().focus().splitCell().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.splitCell') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleHeaderColumn().run()"
|
||||
:disabled="!editor.can().toggleHeaderColumn"
|
||||
@click="editor.chain().focus().toggleHeaderColumn().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.toggleHeaderColumn') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleHeaderRow().run()"
|
||||
:disabled="!editor.can().toggleHeaderRow"
|
||||
@click="editor.chain().focus().toggleHeaderRow().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.toggleHeaderRow') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().toggleHeaderCell().run()"
|
||||
:disabled="!editor.can().toggleHeaderCell"
|
||||
@click="editor.chain().focus().toggleHeaderCell().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.toggleHeaderCell') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().mergeOrSplit().run()"
|
||||
:disabled="!editor.can().mergeOrSplit"
|
||||
@click="editor.chain().focus().mergeOrSplit().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.mergeOrSplit') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-toolbar__button"
|
||||
@click="editor.chain().focus().fixTables().run()"
|
||||
:disabled="!editor.can().fixTables"
|
||||
@click="editor.chain().focus().fixTables().run()"
|
||||
>
|
||||
{{ $t('input.editor.table.fixTables') }}
|
||||
</BaseButton>
|
||||
|
@ -336,6 +339,7 @@ import {ref} from 'vue'
|
|||
import {Editor} from '@tiptap/vue-3'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
|
||||
|
||||
const {
|
||||
editor = null,
|
||||
|
@ -353,29 +357,8 @@ function openImagePicker() {
|
|||
document.getElementById('tiptap__image-upload').click()
|
||||
}
|
||||
|
||||
function setLink() {
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.setLink({href: url, target: '_blank'})
|
||||
.run()
|
||||
function setLink(event) {
|
||||
setLinkInEditor(event.target.getBoundingClientRect(), editor)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div class="tiptap" ref="tiptapInstanceRef">
|
||||
<div
|
||||
ref="tiptapInstanceRef"
|
||||
class="tiptap"
|
||||
>
|
||||
<EditorToolbar
|
||||
v-if="editor && isEditing"
|
||||
:editor="editor"
|
||||
|
@ -11,108 +14,124 @@
|
|||
class="editor-bubble__wrapper"
|
||||
>
|
||||
<BaseButton
|
||||
class="editor-bubble__button"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
v-tooltip="$t('input.editor.bold')"
|
||||
class="editor-bubble__button"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
>
|
||||
<icon :icon="['fa', 'fa-bold']"/>
|
||||
<icon :icon="['fa', 'fa-bold']" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-bubble__button"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
v-tooltip="$t('input.editor.italic')"
|
||||
class="editor-bubble__button"
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
>
|
||||
<icon :icon="['fa', 'fa-italic']"/>
|
||||
<icon :icon="['fa', 'fa-italic']" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-bubble__button"
|
||||
@click="editor.chain().focus().toggleUnderline().run()"
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
v-tooltip="$t('input.editor.underline')"
|
||||
class="editor-bubble__button"
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
@click="editor.chain().focus().toggleUnderline().run()"
|
||||
>
|
||||
<icon :icon="['fa', 'fa-underline']"/>
|
||||
<icon :icon="['fa', 'fa-underline']" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-bubble__button"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
v-tooltip="$t('input.editor.strikethrough')"
|
||||
class="editor-bubble__button"
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
>
|
||||
<icon :icon="['fa', 'fa-strikethrough']"/>
|
||||
<icon :icon="['fa', 'fa-strikethrough']" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-bubble__button"
|
||||
@click="editor.chain().focus().toggleCode().run()"
|
||||
:class="{ 'is-active': editor.isActive('code') }"
|
||||
v-tooltip="$t('input.editor.code')"
|
||||
class="editor-bubble__button"
|
||||
:class="{ 'is-active': editor.isActive('code') }"
|
||||
@click="editor.chain().focus().toggleCode().run()"
|
||||
>
|
||||
<icon :icon="['fa', 'fa-code']"/>
|
||||
<icon :icon="['fa', 'fa-code']" />
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="editor-bubble__button"
|
||||
@click="setLink"
|
||||
:class="{ 'is-active': editor.isActive('link') }"
|
||||
v-tooltip="$t('input.editor.link')"
|
||||
class="editor-bubble__button"
|
||||
:class="{ 'is-active': editor.isActive('link') }"
|
||||
@click="setLink"
|
||||
>
|
||||
<icon :icon="['fa', 'fa-link']"/>
|
||||
<icon :icon="['fa', 'fa-link']" />
|
||||
</BaseButton>
|
||||
</BubbleMenu>
|
||||
|
||||
<editor-content
|
||||
<EditorContent
|
||||
class="tiptap__editor"
|
||||
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
|
||||
:editor="editor"
|
||||
@click="focusIfEditing()"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-if="isEditing"
|
||||
type="file"
|
||||
id="tiptap__image-upload"
|
||||
class="is-hidden"
|
||||
ref="uploadInputRef"
|
||||
type="file"
|
||||
class="is-hidden"
|
||||
@change="addImage"
|
||||
/>
|
||||
>
|
||||
|
||||
<ul class="tiptap__editor-actions d-print-none" v-if="bottomActions.length === 0 && !isEditing">
|
||||
<ul
|
||||
v-if="bottomActions.length === 0 && !isEditing && isEditEnabled"
|
||||
class="tiptap__editor-actions d-print-none"
|
||||
>
|
||||
<li>
|
||||
<BaseButton
|
||||
class="done-edit"
|
||||
@click="setEdit"
|
||||
class="done-edit">
|
||||
>
|
||||
{{ $t('input.editor.edit') }}
|
||||
</BaseButton>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="tiptap__editor-actions d-print-none" v-if="bottomActions.length > 0">
|
||||
<ul
|
||||
v-if="bottomActions.length > 0"
|
||||
class="tiptap__editor-actions d-print-none"
|
||||
>
|
||||
<li v-if="isEditing && showSave">
|
||||
<BaseButton
|
||||
class="done-edit"
|
||||
@click="bubbleSave"
|
||||
class="done-edit">
|
||||
>
|
||||
{{ $t('misc.save') }}
|
||||
</BaseButton>
|
||||
</li>
|
||||
<li v-if="!isEditing">
|
||||
<BaseButton
|
||||
class="done-edit"
|
||||
@click="setEdit"
|
||||
class="done-edit">
|
||||
>
|
||||
{{ $t('input.editor.edit') }}
|
||||
</BaseButton>
|
||||
</li>
|
||||
<li v-for="(action, k) in bottomActions" :key="k">
|
||||
<BaseButton @click="action.action">{{ action.title }}</BaseButton>
|
||||
<li
|
||||
v-for="(action, k) in bottomActions"
|
||||
:key="k"
|
||||
>
|
||||
<BaseButton @click="action.action">
|
||||
{{ action.title }}
|
||||
</BaseButton>
|
||||
</li>
|
||||
</ul>
|
||||
<x-button
|
||||
<XButton
|
||||
v-else-if="isEditing && showSave"
|
||||
v-cy="'saveEditor'"
|
||||
class="mt-4"
|
||||
@click="bubbleSave"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
v-cy="'saveEditor'"
|
||||
:disabled="!contentHasChanged"
|
||||
@click="bubbleSave"
|
||||
>
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</XButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -171,8 +190,29 @@ import XButton from '@/components/input/button.vue'
|
|||
import {Placeholder} from '@tiptap/extension-placeholder'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
import {mergeAttributes} from '@tiptap/core'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
import inputPrompt from '@/helpers/inputPrompt'
|
||||
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
uploadCallback,
|
||||
isEditEnabled = true,
|
||||
bottomActions = [],
|
||||
showSave = false,
|
||||
placeholder = '',
|
||||
editShortcut = '',
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
uploadCallback?: UploadCallback,
|
||||
isEditEnabled?: boolean,
|
||||
bottomActions?: BottomAction[],
|
||||
showSave?: boolean,
|
||||
placeholder?: string,
|
||||
editShortcut?: string,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
|
@ -227,18 +267,19 @@ const CustomImage = Image.extend({
|
|||
renderHTML({HTMLAttributes}) {
|
||||
if (HTMLAttributes.src?.startsWith(window.API_URL) || HTMLAttributes['data-src']?.startsWith(window.API_URL)) {
|
||||
const imageUrl = HTMLAttributes['data-src'] ?? HTMLAttributes.src
|
||||
const id = 'tiptap-image-' + createRandomID()
|
||||
nextTick(async () => {
|
||||
|
||||
const img = document.getElementById(id)
|
||||
|
||||
if (!img) return
|
||||
|
||||
// The url is something like /tasks/<id>/attachments/<id>
|
||||
const parts = imageUrl.slice(window.API_URL.length + 1).split('/')
|
||||
const taskId = Number(parts[1])
|
||||
const attachmentId = Number(parts[3])
|
||||
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
|
||||
const id = 'tiptap-image-' + cacheKey
|
||||
|
||||
nextTick(async () => {
|
||||
|
||||
const img = document.getElementById(id)
|
||||
|
||||
if (!img) return
|
||||
|
||||
if (typeof loadedAttachments.value[cacheKey] === 'undefined') {
|
||||
|
||||
|
@ -266,31 +307,21 @@ const CustomImage = Image.extend({
|
|||
|
||||
type Mode = 'edit' | 'preview'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
uploadCallback,
|
||||
isEditEnabled = true,
|
||||
bottomActions = [],
|
||||
showSave = false,
|
||||
placeholder = '',
|
||||
editShortcut = '',
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
uploadCallback?: UploadCallback,
|
||||
isEditEnabled?: boolean,
|
||||
bottomActions?: BottomAction[],
|
||||
showSave?: boolean,
|
||||
placeholder?: string,
|
||||
editShortcut?: string,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const internalMode = ref<Mode>('edit')
|
||||
const internalMode = ref<Mode>('preview')
|
||||
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
||||
const contentHasChanged = ref<boolean>(false)
|
||||
|
||||
watch(
|
||||
() => internalMode.value,
|
||||
mode => {
|
||||
if (mode === 'preview') {
|
||||
contentHasChanged.value = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const editor = useEditor({
|
||||
content: modelValue,
|
||||
// eslint-disable-next-line vue/no-ref-object-destructure
|
||||
editable: isEditing.value,
|
||||
extensions: [
|
||||
// Starterkit:
|
||||
|
@ -308,7 +339,9 @@ const editor = useEditor({
|
|||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-Enter': () => {
|
||||
if (contentHasChanged.value) {
|
||||
bubbleSave()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -420,6 +453,7 @@ function bubbleNow() {
|
|||
return
|
||||
}
|
||||
|
||||
contentHasChanged.value = true
|
||||
emit('update:modelValue', editor.value?.getHTML())
|
||||
}
|
||||
|
||||
|
@ -455,7 +489,7 @@ function uploadAndInsertFiles(files: File[] | FileList) {
|
|||
})
|
||||
}
|
||||
|
||||
function addImage() {
|
||||
async function addImage(event) {
|
||||
|
||||
if (typeof uploadCallback !== 'undefined') {
|
||||
const files = uploadInputRef.value?.files
|
||||
|
@ -469,7 +503,7 @@ function addImage() {
|
|||
return
|
||||
}
|
||||
|
||||
const url = window.prompt('URL')
|
||||
const url = await inputPrompt(event.target.getBoundingClientRect())
|
||||
|
||||
if (url) {
|
||||
editor.value?.chain().focus().setImage({src: url}).run()
|
||||
|
@ -477,34 +511,8 @@ function addImage() {
|
|||
}
|
||||
}
|
||||
|
||||
function setLink() {
|
||||
const previousUrl = editor.value?.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.value
|
||||
?.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.unsetLink()
|
||||
.run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor.value
|
||||
?.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.setLink({href: url, target: '_blank'})
|
||||
.run()
|
||||
function setLink(event) {
|
||||
setLinkInEditor(event.target.getBoundingClientRect(), editor.value)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
@ -558,6 +566,7 @@ function setFocusToEditor(event) {
|
|||
event.target.contentEditable === 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
if (!isEditing.value && isEditEnabled) {
|
||||
|
@ -567,6 +576,12 @@ function setFocusToEditor(event) {
|
|||
editor.value?.commands.focus()
|
||||
}
|
||||
|
||||
function focusIfEditing() {
|
||||
if (isEditing.value) {
|
||||
editor.value?.commands.focus()
|
||||
}
|
||||
}
|
||||
|
||||
function clickTasklistCheckbox(event) {
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
|
@ -748,7 +763,7 @@ watch(
|
|||
height: auto;
|
||||
|
||||
&.ProseMirror-selectednode {
|
||||
outline: 3px solid #68cef8;
|
||||
outline: 3px solid var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
26
src/components/input/editor/setLinkInEditor.ts
Normal file
26
src/components/input/editor/setLinkInEditor.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import inputPrompt from '@/helpers/inputPrompt'
|
||||
|
||||
export async function setLinkInEditor(pos, editor) {
|
||||
const previousUrl = editor?.getAttributes('link').href || ''
|
||||
const url = await inputPrompt(pos, previousUrl)
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.unsetLink()
|
||||
.run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor
|
||||
?.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.setLink({href: url, target: '_blank'})
|
||||
.run()
|
||||
}
|
|
@ -26,32 +26,42 @@ const withoutInitialState = ref<boolean | undefined>()
|
|||
</FancyCheckbox>
|
||||
|
||||
Visualisation
|
||||
<input type="checkbox" v-model="isChecked">
|
||||
<input
|
||||
v-model="isChecked"
|
||||
type="checkbox"
|
||||
>
|
||||
{{ isChecked }}
|
||||
</Variant>
|
||||
<Variant title="Enabled Initially">
|
||||
<FancyCheckbox
|
||||
:disabled="isDisabled"
|
||||
v-model="isCheckedInitiallyEnabled"
|
||||
:disabled="isDisabled"
|
||||
>
|
||||
We want you to use this option
|
||||
</FancyCheckbox>
|
||||
|
||||
Visualisation
|
||||
<input type="checkbox" v-model="isCheckedInitiallyEnabled">
|
||||
<input
|
||||
v-model="isCheckedInitiallyEnabled"
|
||||
type="checkbox"
|
||||
>
|
||||
{{ isCheckedInitiallyEnabled }}
|
||||
</Variant>
|
||||
<Variant title="Disabled">
|
||||
<FancyCheckbox
|
||||
disabled
|
||||
:modelValue="isCheckedDisabled"
|
||||
@update:model-value="logEvent('Setting disabled: This should never happen', $event)"
|
||||
:model-value="isCheckedDisabled"
|
||||
@update:modelValue="logEvent('Setting disabled: This should never happen', $event)"
|
||||
>
|
||||
You can't change this
|
||||
</FancyCheckbox>
|
||||
|
||||
Visualisation
|
||||
<input type="checkbox" v-model="isCheckedDisabled" disabled>
|
||||
<input
|
||||
v-model="isCheckedDisabled"
|
||||
type="checkbox"
|
||||
disabled
|
||||
>
|
||||
{{ isCheckedDisabled }}
|
||||
</Variant>
|
||||
|
||||
|
@ -64,7 +74,11 @@ const withoutInitialState = ref<boolean | undefined>()
|
|||
</FancyCheckbox>
|
||||
|
||||
Visualisation
|
||||
<input type="checkbox" v-model="withoutInitialState" disabled>
|
||||
<input
|
||||
v-model="withoutInitialState"
|
||||
type="checkbox"
|
||||
disabled
|
||||
>
|
||||
{{ withoutInitialState }}
|
||||
</Variant>
|
||||
</Story>
|
||||
|
|
|
@ -7,11 +7,14 @@
|
|||
}"
|
||||
:disabled="disabled"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="value => emit('update:modelValue', value)"
|
||||
@update:modelValue="value => emit('update:modelValue', value)"
|
||||
>
|
||||
<CheckboxIcon class="fancycheckbox__icon" />
|
||||
<span v-if="$slots.default" class="fancycheckbox__content">
|
||||
<slot/>
|
||||
<span
|
||||
v-if="$slots.default"
|
||||
class="fancycheckbox__content"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</BaseCheckbox>
|
||||
</template>
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<template>
|
||||
<div
|
||||
ref="multiselectRoot"
|
||||
class="multiselect"
|
||||
:class="{'has-search-results': searchResultsVisible}"
|
||||
ref="multiselectRoot"
|
||||
tabindex="-1"
|
||||
@focus="focus"
|
||||
>
|
||||
<div class="control" :class="{'is-loading': loading || localLoading}">
|
||||
<div
|
||||
class="control"
|
||||
:class="{'is-loading': loading || localLoading}"
|
||||
>
|
||||
<div
|
||||
class="input-wrapper input"
|
||||
:class="{'has-multiple': hasMultiple, 'has-removal-button': removalAvailable}"
|
||||
|
@ -18,51 +21,67 @@
|
|||
:remove="remove"
|
||||
>
|
||||
<template v-for="(item, key) in internalValue">
|
||||
<slot name="tag" :item="item">
|
||||
<span :key="`item${key}`" class="tag ml-2 mt-2">
|
||||
<slot
|
||||
name="tag"
|
||||
:item="item"
|
||||
>
|
||||
<span
|
||||
:key="`item${key}`"
|
||||
class="tag ml-2 mt-2"
|
||||
>
|
||||
{{ label !== '' ? item[label] : item }}
|
||||
<BaseButton @click="() => remove(item)" class="delete is-small"></BaseButton>
|
||||
<BaseButton
|
||||
class="delete is-small"
|
||||
@click="() => remove(item)"
|
||||
/>
|
||||
</span>
|
||||
</slot>
|
||||
</template>
|
||||
</slot>
|
||||
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="query"
|
||||
type="text"
|
||||
class="input"
|
||||
v-model="query"
|
||||
@keyup="search"
|
||||
@keyup.enter.exact.prevent="() => createOrSelectOnEnter()"
|
||||
:placeholder="placeholder"
|
||||
@keydown.down.exact.prevent="() => preSelect(0)"
|
||||
ref="searchInput"
|
||||
@focus="handleFocus"
|
||||
:autocomplete="autocompleteEnabled ? undefined : 'off'"
|
||||
:spellcheck="autocompleteEnabled ? undefined : 'false'"
|
||||
/>
|
||||
@keyup="search"
|
||||
@keyup.enter.exact.prevent="() => createOrSelectOnEnter()"
|
||||
@keydown.down.exact.prevent="() => preSelect(0)"
|
||||
@focus="handleFocus"
|
||||
>
|
||||
<BaseButton
|
||||
v-if="removalAvailable"
|
||||
class="removal-button"
|
||||
@click="resetSelectedValue"
|
||||
>
|
||||
<icon icon="times"/>
|
||||
<icon icon="times" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CustomTransition name="fade">
|
||||
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
|
||||
<div
|
||||
v-if="searchResultsVisible"
|
||||
class="search-results"
|
||||
:class="{'search-results-inline': inline}"
|
||||
>
|
||||
<BaseButton
|
||||
class="search-result-button is-fullwidth"
|
||||
v-for="(data, index) in filteredSearchResults"
|
||||
:key="index"
|
||||
:ref="(el) => setResult(el, index)"
|
||||
class="search-result-button is-fullwidth"
|
||||
@keydown.up.prevent="() => preSelect(index - 1)"
|
||||
@keydown.down.prevent="() => preSelect(index + 1)"
|
||||
@click.prevent.stop="() => select(data)"
|
||||
>
|
||||
<span>
|
||||
<slot name="searchResult" :option="data">
|
||||
<slot
|
||||
name="searchResult"
|
||||
:option="data"
|
||||
>
|
||||
<span class="search-result">{{ label !== '' ? data[label] : data }}</span>
|
||||
</slot>
|
||||
</span>
|
||||
|
@ -73,15 +92,18 @@
|
|||
|
||||
<BaseButton
|
||||
v-if="creatableAvailable"
|
||||
class="search-result-button is-fullwidth"
|
||||
:ref="(el) => setResult(el, filteredSearchResults.length)"
|
||||
class="search-result-button is-fullwidth"
|
||||
@keydown.up.prevent="() => preSelect(filteredSearchResults.length - 1)"
|
||||
@keydown.down.prevent="() => preSelect(filteredSearchResults.length + 1)"
|
||||
@keyup.enter.prevent="create"
|
||||
@click.prevent.stop="create"
|
||||
>
|
||||
<span>
|
||||
<slot name="searchResult" :option="query">
|
||||
<slot
|
||||
name="searchResult"
|
||||
:option="query"
|
||||
>
|
||||
<span class="search-result">
|
||||
{{ query }}
|
||||
</span>
|
||||
|
@ -107,16 +129,6 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
|||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function elementInResults(elem: string | any, label: string, query: string): boolean {
|
||||
// Don't make create available if we have an exact match in our search results.
|
||||
if (label !== '') {
|
||||
return elem[label] === query
|
||||
}
|
||||
|
||||
return elem === query
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* When true, shows a loading spinner
|
||||
|
@ -245,6 +257,16 @@ const emit = defineEmits<{
|
|||
(e: 'remove', value: null): void
|
||||
}>()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function elementInResults(elem: string | any, label: string, query: string): boolean {
|
||||
// Don't make create available if we have an exact match in our search results.
|
||||
if (label !== '') {
|
||||
return elem[label] === query
|
||||
}
|
||||
|
||||
return elem === query
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const query = ref<string | { [key: string]: any }>('')
|
||||
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
<template>
|
||||
<div class="password-field">
|
||||
<input
|
||||
class="input"
|
||||
id="password"
|
||||
class="input"
|
||||
name="password"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
required
|
||||
:type="passwordFieldType"
|
||||
autocomplete="current-password"
|
||||
@keyup.enter="e => $emit('submit', e)"
|
||||
:tabindex="props.tabindex"
|
||||
@keyup.enter="e => $emit('submit', e)"
|
||||
@focusout="validate"
|
||||
@input="handleInput"
|
||||
/>
|
||||
>
|
||||
<BaseButton
|
||||
@click="togglePasswordFieldType"
|
||||
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
||||
class="password-field-type-toggle"
|
||||
:aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
||||
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
||||
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
||||
@click="togglePasswordFieldType"
|
||||
>
|
||||
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="!isValid">
|
||||
<p
|
||||
v-if="!isValid"
|
||||
class="help is-danger"
|
||||
>
|
||||
{{ $t('user.auth.passwordRequired') }}
|
||||
</p>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<BaseButton class="button-link"><slot/></BaseButton>
|
||||
<BaseButton class="button-link">
|
||||
<slot />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -31,10 +31,10 @@ function openQuickActions() {
|
|||
|
||||
<template>
|
||||
<BaseButton
|
||||
@click="openQuickActions"
|
||||
class="trigger-button"
|
||||
:title="$t('keyboardShortcuts.quickSearch')"
|
||||
@click="openQuickActions"
|
||||
>
|
||||
<icon icon="search"/>
|
||||
<icon icon="search" />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
|
15
src/components/misc/ProgressBar.story.vue
Normal file
15
src/components/misc/ProgressBar.story.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
|
||||
import ProgressBar from './ProgressBar.vue'
|
||||
|
||||
const value = ref(50)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story>
|
||||
<Variant title="Default">
|
||||
<ProgressBar :value="value" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
139
src/components/misc/ProgressBar.vue
Normal file
139
src/components/misc/ProgressBar.vue
Normal file
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<progress
|
||||
class="progress-bar"
|
||||
:class="{
|
||||
'is-small': isSmall,
|
||||
'is-primary': isPrimary,
|
||||
}"
|
||||
:value="value"
|
||||
max="100"
|
||||
>
|
||||
{{ value }}%
|
||||
</progress>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {defineProps} from 'vue'
|
||||
|
||||
defineProps({
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isSmall: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isPrimary: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.progress-bar {
|
||||
--progress-height: #{$size-normal};
|
||||
--progress-bar-background-color: var(--border-light, #{$border-light});
|
||||
--progress-value-background-color: var(--grey-500, #{$text});
|
||||
--progress-border-radius: #{$radius};
|
||||
--progress-indeterminate-duration: 1.5s;
|
||||
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: var(--progress-border-radius);
|
||||
height: var(--progress-height);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
min-width: 6vw;
|
||||
|
||||
width: 50px;
|
||||
margin: 0 .5rem 0 0;
|
||||
flex: 3 1 auto;
|
||||
|
||||
&::-moz-progress-bar,
|
||||
&::-webkit-progress-value {
|
||||
background: var(--progress-value-background-color);
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin: 0.5rem 0 0 0;
|
||||
order: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: var(--progress-bar-background-color);
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: var(--progress-value-background-color);
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: var(--progress-value-background-color);
|
||||
}
|
||||
|
||||
&::-ms-fill {
|
||||
background-color: var(--progress-value-background-color);
|
||||
border: none;
|
||||
}
|
||||
|
||||
// Colors
|
||||
@each $name, $pair in $colors {
|
||||
$color: nth($pair, 1);
|
||||
&.is-#{$name} {
|
||||
--progress-value-background-color: var(--#{$name}, #{$color});
|
||||
|
||||
&:indeterminate {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--#{$name}, #{$color}) 30%,
|
||||
var(--progress-bar-background-color) 30%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:indeterminate {
|
||||
animation-duration: var(--progress-indeterminate-duration);
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: moveIndeterminate;
|
||||
animation-timing-function: linear;
|
||||
background-color: var(--progress-bar-background-color);
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--text, #{$text}) 30%,
|
||||
var(--progress-bar-background-color) 30%
|
||||
);
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 150% 150%;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-ms-fill {
|
||||
animation-name: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
--progress-height: #{$size-small};
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveIndeterminate {
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,38 +1,62 @@
|
|||
<template>
|
||||
<div class="api-config">
|
||||
<div v-if="configureApi">
|
||||
<label class="label" for="api-url">{{ $t('apiConfig.url') }}</label>
|
||||
<label
|
||||
class="label"
|
||||
for="api-url"
|
||||
>{{ $t('apiConfig.url') }}</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
id="api-url"
|
||||
v-model="apiUrl"
|
||||
v-focus
|
||||
class="input"
|
||||
:placeholder="$t('apiConfig.urlPlaceholder')"
|
||||
required
|
||||
type="url"
|
||||
v-focus
|
||||
v-model="apiUrl"
|
||||
@keyup.enter="setApiUrl"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button @click="setApiUrl" :disabled="apiUrl === '' || undefined">
|
||||
<x-button
|
||||
:disabled="apiUrl === '' || undefined"
|
||||
@click="setApiUrl"
|
||||
>
|
||||
{{ $t('apiConfig.change') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-url-info" v-else>
|
||||
<i18n-t keypath="apiConfig.use" scope="global">
|
||||
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
|
||||
<div
|
||||
v-else
|
||||
class="api-url-info"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="apiConfig.use"
|
||||
scope="global"
|
||||
>
|
||||
<span
|
||||
v-tooltip="apiUrl"
|
||||
class="url"
|
||||
> {{ apiDomain }} </span>
|
||||
</i18n-t>
|
||||
<br/>
|
||||
<ButtonLink class="api-config__change-button" @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</ButtonLink>
|
||||
<br>
|
||||
<ButtonLink
|
||||
class="api-config__change-button"
|
||||
@click="() => (configureApi = true)"
|
||||
>
|
||||
{{ $t('apiConfig.change') }}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
|
||||
<message variant="danger" v-if="errorMsg !== ''" class="mt-2">
|
||||
<Message
|
||||
v-if="errorMsg !== ''"
|
||||
variant="danger"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ errorMsg }}
|
||||
</message>
|
||||
</Message>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -57,7 +81,7 @@ const props = defineProps({
|
|||
const emit = defineEmits(['foundApi'])
|
||||
|
||||
const apiUrl = ref(window.API_URL)
|
||||
const configureApi = ref(apiUrl.value === '')
|
||||
const configureApi = ref(window.API_URL === '')
|
||||
|
||||
// Because we're only using this to parse the hostname, it should be fine to just prefix with http://
|
||||
// regardless of whether the url is actually reachable under http.
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
<template>
|
||||
<div class="card" :class="{'has-no-shadow': !shadow}">
|
||||
<header class="card-header" v-if="title !== ''">
|
||||
<div
|
||||
class="card"
|
||||
:class="{'has-no-shadow': !shadow}"
|
||||
>
|
||||
<header
|
||||
v-if="title !== ''"
|
||||
class="card-header"
|
||||
>
|
||||
<p class="card-header-title">
|
||||
{{ title }}
|
||||
</p>
|
||||
<BaseButton
|
||||
v-if="hasClose"
|
||||
v-tooltip="$t('misc.close')"
|
||||
class="card-header-icon"
|
||||
:aria-label="$t('misc.close')"
|
||||
@click="$emit('close')"
|
||||
v-tooltip="$t('misc.close')"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="closeIcon"/>
|
||||
<icon :icon="closeIcon" />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</header>
|
||||
|
@ -24,12 +30,15 @@
|
|||
}"
|
||||
>
|
||||
<div :class="{'content': hasContent}">
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer v-if="$slots.footer" class="card-footer">
|
||||
<slot name="footer"/>
|
||||
<footer
|
||||
v-if="$slots.footer"
|
||||
class="card-footer"
|
||||
>
|
||||
<slot name="footer" />
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<span
|
||||
:style="{backgroundColor: color }"
|
||||
class="color-bubble"
|
||||
></span>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
<template>
|
||||
<modal @close="$router.back()" :overflow="true" :wide="wide">
|
||||
<modal
|
||||
:overflow="true"
|
||||
:wide="wide"
|
||||
@close="$router.back()"
|
||||
>
|
||||
<card
|
||||
:title="title"
|
||||
:shadow="false"
|
||||
:padding="false"
|
||||
class="has-text-left"
|
||||
:has-close="true"
|
||||
@close="$router.back()"
|
||||
:loading="loading"
|
||||
@close="$router.back()"
|
||||
>
|
||||
<div class="p-4">
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
|
@ -32,10 +36,10 @@
|
|||
<x-button
|
||||
v-if="hasPrimaryAction"
|
||||
variant="primary"
|
||||
@click.prevent.stop="primary()"
|
||||
:icon="primaryIcon"
|
||||
:disabled="primaryDisabled || loading"
|
||||
class="ml-2"
|
||||
@click.prevent.stop="primary()"
|
||||
>
|
||||
{{ primaryLabel || $t('misc.create') }}
|
||||
</x-button>
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
class="icon is-small"
|
||||
:class="iconClass"
|
||||
>
|
||||
<Icon :icon="icon"/>
|
||||
<Icon :icon="icon" />
|
||||
</span>
|
||||
<span>
|
||||
<slot/>
|
||||
<slot />
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
|
|
@ -1,15 +1,32 @@
|
|||
<template>
|
||||
<div class="dropdown" ref="dropdown">
|
||||
<slot name="trigger" :close="close" :toggleOpen="toggleOpen" :open="open">
|
||||
<BaseButton class="dropdown-trigger is-flex" @click="toggleOpen">
|
||||
<icon :icon="triggerIcon" class="icon"/>
|
||||
<div
|
||||
ref="dropdown"
|
||||
class="dropdown"
|
||||
>
|
||||
<slot
|
||||
name="trigger"
|
||||
:close="close"
|
||||
:toggle-open="toggleOpen"
|
||||
:open="open"
|
||||
>
|
||||
<BaseButton
|
||||
class="dropdown-trigger is-flex"
|
||||
@click="toggleOpen"
|
||||
>
|
||||
<icon
|
||||
:icon="triggerIcon"
|
||||
class="icon"
|
||||
/>
|
||||
</BaseButton>
|
||||
</slot>
|
||||
|
||||
<CustomTransition name="fade">
|
||||
<div class="dropdown-menu" v-if="open">
|
||||
<div
|
||||
v-if="open"
|
||||
class="dropdown-menu"
|
||||
>
|
||||
<div class="dropdown-content">
|
||||
<slot :close="close"></slot>
|
||||
<slot :close="close" />
|
||||
</div>
|
||||
</div>
|
||||
</CustomTransition>
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
<template>
|
||||
<message variant="danger">
|
||||
<i18n-t keypath="loadingError.failed" scope="global">
|
||||
<ButtonLink @click="reload">{{ $t('loadingError.tryAgain') }}</ButtonLink>
|
||||
<ButtonLink href="https://vikunja.io/contact/">{{ $t('loadingError.contact') }}</ButtonLink>
|
||||
<Message variant="danger">
|
||||
<i18n-t
|
||||
keypath="loadingError.failed"
|
||||
scope="global"
|
||||
>
|
||||
<ButtonLink @click="reload">
|
||||
{{ $t('loadingError.tryAgain') }}
|
||||
</ButtonLink>
|
||||
<ButtonLink href="https://vikunja.io/contact/">
|
||||
{{ $t('loadingError.contact') }}
|
||||
</ButtonLink>
|
||||
</i18n-t>
|
||||
</message>
|
||||
</Message>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<input
|
||||
v-bind="attrs"
|
||||
ref="root"
|
||||
type="text"
|
||||
data-input
|
||||
:disabled="disabled"
|
||||
v-bind="attrs"
|
||||
ref="root"
|
||||
/>
|
||||
>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
<template>
|
||||
<modal @close="close()">
|
||||
<card class="has-background-white keyboard-shortcuts" :shadow="false" :title="$t('keyboardShortcuts.title')">
|
||||
<template v-for="(s, i) in shortcuts" :key="i">
|
||||
<card
|
||||
class="has-background-white keyboard-shortcuts"
|
||||
:shadow="false"
|
||||
:title="$t('keyboardShortcuts.title')"
|
||||
>
|
||||
<template
|
||||
v-for="(s, i) in shortcuts"
|
||||
:key="i"
|
||||
>
|
||||
<h3>{{ $t(s.title) }}</h3>
|
||||
|
||||
<message class="mb-4" v-if="s.available">
|
||||
<Message
|
||||
v-if="s.available"
|
||||
class="mb-4"
|
||||
>
|
||||
{{
|
||||
typeof s.available === 'undefined' ?
|
||||
$t('keyboardShortcuts.allPages') :
|
||||
|
@ -14,14 +24,19 @@
|
|||
: $t('keyboardShortcuts.somePagesOnly')
|
||||
)
|
||||
}}
|
||||
</message>
|
||||
</Message>
|
||||
|
||||
<dl class="shortcut-list">
|
||||
<template v-for="(sc, si) in s.shortcuts" :key="si">
|
||||
<dt class="shortcut-title">{{ $t(sc.title) }}</dt>
|
||||
<shortcut
|
||||
class="shortcut-keys"
|
||||
<template
|
||||
v-for="(sc, si) in s.shortcuts"
|
||||
:key="si"
|
||||
>
|
||||
<dt class="shortcut-title">
|
||||
{{ $t(sc.title) }}
|
||||
</dt>
|
||||
<Shortcut
|
||||
is="dd"
|
||||
class="shortcut-keys"
|
||||
:keys="sc.keys"
|
||||
:combination="sc.combination && $t(`keyboardShortcuts.${sc.combination}`)"
|
||||
/>
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
<template>
|
||||
<div class="legal-links">
|
||||
<BaseButton :href="imprintUrl" v-if="imprintUrl">{{ $t('navigation.imprint') }}</BaseButton>
|
||||
<BaseButton
|
||||
v-if="imprintUrl"
|
||||
:href="imprintUrl"
|
||||
>
|
||||
{{ $t('navigation.imprint') }}
|
||||
</BaseButton>
|
||||
<span v-if="imprintUrl && privacyPolicyUrl"> | </span>
|
||||
<BaseButton :href="privacyPolicyUrl" v-if="privacyPolicyUrl">{{ $t('navigation.privacy') }}</BaseButton>
|
||||
<BaseButton
|
||||
v-if="privacyPolicyUrl"
|
||||
:href="privacyPolicyUrl"
|
||||
>
|
||||
{{ $t('navigation.privacy') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div class="loader-container is-loading" :class="{'is-small': variant === 'small'}"></div>
|
||||
<div
|
||||
class="loader-container is-loading"
|
||||
:class="{'is-small': variant === 'small'}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<div class="message-wrapper">
|
||||
<div class="message" :class="[variant, textAlignClass]">
|
||||
<slot/>
|
||||
<div
|
||||
class="message"
|
||||
:class="[variant, textAlignClass]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -9,14 +12,6 @@
|
|||
<script lang="ts" setup>
|
||||
import {computed, type PropType} from 'vue'
|
||||
|
||||
const TEXT_ALIGN_MAP = Object.freeze({
|
||||
left: '',
|
||||
center: 'has-text-centered',
|
||||
right: 'has-text-right',
|
||||
})
|
||||
|
||||
type textAlignVariants = keyof typeof TEXT_ALIGN_MAP
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
|
@ -28,6 +23,14 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const TEXT_ALIGN_MAP = Object.freeze({
|
||||
left: '',
|
||||
center: 'has-text-centered',
|
||||
right: 'has-text-right',
|
||||
})
|
||||
|
||||
type textAlignVariants = keyof typeof TEXT_ALIGN_MAP
|
||||
|
||||
const textAlignClass = computed(() => TEXT_ALIGN_MAP[props.textAlign])
|
||||
|
||||
</script>
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- FIXME: transition should not be included in the modal -->
|
||||
<CustomTransition :name="transitionName" appear>
|
||||
<CustomTransition
|
||||
:name="transitionName"
|
||||
appear
|
||||
>
|
||||
<section
|
||||
v-if="enabled"
|
||||
ref="modal"
|
||||
class="modal-mask"
|
||||
:class="[
|
||||
{ 'has-overflow': overflow },
|
||||
variant,
|
||||
]"
|
||||
ref="modal"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<div
|
||||
v-shortcut="'Escape'"
|
||||
class="modal-container"
|
||||
@mousedown.self.prevent.stop="$emit('close')"
|
||||
v-shortcut="'Escape'"
|
||||
>
|
||||
<div
|
||||
class="modal-content"
|
||||
|
@ -25,32 +28,32 @@
|
|||
}"
|
||||
>
|
||||
<BaseButton
|
||||
@click="$emit('close')"
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<icon icon="times"/>
|
||||
<icon icon="times" />
|
||||
</BaseButton>
|
||||
|
||||
<slot>
|
||||
<div class="header">
|
||||
<slot name="header"></slot>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot name="text"></slot>
|
||||
<slot name="text" />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<x-button
|
||||
@click="$emit('close')"
|
||||
variant="tertiary"
|
||||
class="has-text-danger"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="$emit('submit')"
|
||||
variant="primary"
|
||||
v-cy="'modalPrimary'"
|
||||
variant="primary"
|
||||
:shadow="false"
|
||||
@click="$emit('submit')"
|
||||
>
|
||||
{{ $t('misc.doit') }}
|
||||
</x-button>
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
<template>
|
||||
<div class="no-auth-wrapper">
|
||||
<Logo class="logo" width="200" height="58"/>
|
||||
<Logo
|
||||
class="logo"
|
||||
width="200"
|
||||
height="58"
|
||||
/>
|
||||
<div class="noauth-container">
|
||||
<section class="image" :class="{'has-message': motd !== ''}">
|
||||
<section
|
||||
class="image"
|
||||
:class="{'has-message': motd !== ''}"
|
||||
>
|
||||
<Message v-if="motd !== ''">
|
||||
{{ motd }}
|
||||
</Message>
|
||||
|
@ -12,14 +19,22 @@
|
|||
</section>
|
||||
<section class="content">
|
||||
<div>
|
||||
<h2 class="title" v-if="title">{{ title }}</h2>
|
||||
<api-config v-if="showApiConfig"/>
|
||||
<Message v-if="motd !== ''" class="is-hidden-tablet mb-4">
|
||||
<h2
|
||||
v-if="title"
|
||||
class="title"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<ApiConfig v-if="showApiConfig" />
|
||||
<Message
|
||||
v-if="motd !== ''"
|
||||
class="is-hidden-tablet mb-4"
|
||||
>
|
||||
{{ motd }}
|
||||
</Message>
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
<legal/>
|
||||
<Legal />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,6 +53,11 @@ import ApiConfig from '@/components/misc/api-config.vue'
|
|||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
const {
|
||||
showApiConfig = true,
|
||||
} = defineProps<{
|
||||
showApiConfig?: boolean
|
||||
}>()
|
||||
const configStore = useConfigStore()
|
||||
const motd = computed(() => configStore.motd)
|
||||
|
||||
|
@ -46,11 +66,6 @@ const {t} = useI18n({useScope: 'global'})
|
|||
const title = computed(() => t(route.meta?.title as string || ''))
|
||||
useTitle(() => title.value)
|
||||
|
||||
const {
|
||||
showApiConfig = true,
|
||||
} = defineProps<{
|
||||
showApiConfig?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<p class="has-text-centered has-text-grey is-italic p-4 mb-4">
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
|
@ -1,30 +1,43 @@
|
|||
<template>
|
||||
<notifications position="bottom left" :max="2" class="global-notification">
|
||||
<notifications
|
||||
position="bottom left"
|
||||
:max="2"
|
||||
class="global-notification"
|
||||
>
|
||||
<template #body="{ item, close }">
|
||||
<!-- FIXME: overlay whole notification with button and add event listener on that button instead -->
|
||||
<div
|
||||
class="vue-notification-template vue-notification"
|
||||
:class="[
|
||||
'vue-notification-template',
|
||||
'vue-notification',
|
||||
item.type,
|
||||
]"
|
||||
@click="close()"
|
||||
>
|
||||
<div v-if="item.title" class="notification-title">{{ item.title }}</div>
|
||||
<div
|
||||
v-if="item.title"
|
||||
class="notification-title"
|
||||
>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<template v-for="(t, k) in item.text" :key="k">{{ t }}<br /></template>
|
||||
<template
|
||||
v-for="(t, k) in item.text"
|
||||
:key="k"
|
||||
>
|
||||
{{ t }}<br>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
class="buttons is-right"
|
||||
v-if="item.data?.actions?.length > 0"
|
||||
class="buttons is-right"
|
||||
>
|
||||
<x-button
|
||||
v-for="(action, i) in item.data.actions"
|
||||
:key="'action_' + i"
|
||||
@click="action.callback"
|
||||
:shadow="false"
|
||||
class="is-small"
|
||||
variant="secondary"
|
||||
v-for="(action, i) in item.data.actions"
|
||||
@click="action.callback"
|
||||
>
|
||||
{{ action.title }}
|
||||
</x-button>
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
<template>
|
||||
<nav
|
||||
v-if="totalPages > 1"
|
||||
aria-label="pagination"
|
||||
class="pagination is-centered p-4"
|
||||
role="navigation"
|
||||
v-if="totalPages > 1"
|
||||
>
|
||||
<router-link
|
||||
:disabled="currentPage === 1 || undefined"
|
||||
:to="getRouteForPagination(currentPage - 1)"
|
||||
class="pagination-previous">
|
||||
class="pagination-previous"
|
||||
>
|
||||
{{ $t('misc.previous') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
:disabled="currentPage === totalPages || undefined"
|
||||
:to="getRouteForPagination(currentPage + 1)"
|
||||
class="pagination-next">
|
||||
class="pagination-next"
|
||||
>
|
||||
{{ $t('misc.next') }}
|
||||
</router-link>
|
||||
<ul class="pagination-list">
|
||||
<li :key="`page-${i}`" v-for="(p, i) in pages">
|
||||
<span class="pagination-ellipsis" v-if="p.isEllipsis">…</span>
|
||||
<li
|
||||
v-for="(p, i) in pages"
|
||||
:key="`page-${i}`"
|
||||
>
|
||||
<span
|
||||
v-if="p.isEllipsis"
|
||||
class="pagination-ellipsis"
|
||||
>…</span>
|
||||
<router-link
|
||||
v-else
|
||||
class="pagination-link"
|
||||
|
@ -37,6 +45,17 @@
|
|||
<script lang="ts" setup>
|
||||
import {computed} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
totalPages: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
function createPagination(totalPages: number, currentPage: number) {
|
||||
const pages = []
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
|
@ -81,17 +100,6 @@ function getRouteForPagination(page = 1, type = null) {
|
|||
}
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
totalPages: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
<template>
|
||||
<slot name="trigger" :isOpen="open" :toggle="toggle" :close="close"></slot>
|
||||
<slot
|
||||
name="trigger"
|
||||
:is-open="open"
|
||||
:toggle="toggle"
|
||||
:close="close"
|
||||
/>
|
||||
<div
|
||||
ref="popup"
|
||||
class="popup"
|
||||
:class="{
|
||||
'is-open': open,
|
||||
'has-overflow': props.hasOverflow && open
|
||||
}"
|
||||
ref="popup"
|
||||
>
|
||||
<slot name="content" :isOpen="open" :toggle="toggle" :close="close"/>
|
||||
<slot
|
||||
name="content"
|
||||
:is-open="open"
|
||||
:toggle="toggle"
|
||||
:close="close"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -53,6 +63,7 @@ onClickOutside(popup, () => {
|
|||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
z-index: 100;
|
||||
|
||||
&.is-open {
|
||||
opacity: 1;
|
||||
|
|
|
@ -1,37 +1,55 @@
|
|||
<template>
|
||||
<!-- 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>
|
||||
<div class="app offline" v-if="!online">
|
||||
<div
|
||||
class="offline"
|
||||
style="height: 0;width: 0;"
|
||||
/>
|
||||
<div
|
||||
v-if="!online"
|
||||
class="app offline"
|
||||
>
|
||||
<div class="offline-message">
|
||||
<h1 class="title">{{ $t('offline.title') }}</h1>
|
||||
<h1 class="title">
|
||||
{{ $t('offline.title') }}
|
||||
</h1>
|
||||
<p>{{ $t('offline.text') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else-if="ready">
|
||||
<slot/>
|
||||
<slot />
|
||||
</template>
|
||||
<section v-else-if="error !== ''">
|
||||
<no-auth-wrapper :show-api-config="false">
|
||||
<NoAuthWrapper :show-api-config="false">
|
||||
<p v-if="error === ERROR_NO_API_URL">
|
||||
{{ $t('ready.noApiUrlConfigured') }}
|
||||
</p>
|
||||
<message variant="danger" v-else class="mb-4">
|
||||
<Message
|
||||
v-else
|
||||
variant="danger"
|
||||
class="mb-4"
|
||||
>
|
||||
<p>
|
||||
{{ $t('ready.errorOccured') }}<br/>
|
||||
{{ $t('ready.errorOccured') }}<br>
|
||||
{{ error }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('ready.checkApiUrl') }}
|
||||
</p>
|
||||
</message>
|
||||
<api-config :configure-open="true" @found-api="load"/>
|
||||
</no-auth-wrapper>
|
||||
</Message>
|
||||
<ApiConfig
|
||||
:configure-open="true"
|
||||
@foundApi="load"
|
||||
/>
|
||||
</NoAuthWrapper>
|
||||
</section>
|
||||
<CustomTransition name="fade">
|
||||
<section class="vikunja-loading" v-if="showLoading">
|
||||
<Logo class="logo"/>
|
||||
<section
|
||||
v-if="showLoading"
|
||||
class="vikunja-loading"
|
||||
>
|
||||
<Logo class="logo" />
|
||||
<p>
|
||||
<span class="loader-container is-loading-small is-loading"></span>
|
||||
<span class="loader-container is-loading-small is-loading" />
|
||||
{{ $t('ready.loading') }}
|
||||
</p>
|
||||
</section>
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
<template>
|
||||
<component :is="is" class="shortcuts">
|
||||
<template v-for="(k, i) in keys" :key="i">
|
||||
<component
|
||||
:is="is"
|
||||
class="shortcuts"
|
||||
>
|
||||
<template
|
||||
v-for="(k, i) in keys"
|
||||
:key="i"
|
||||
>
|
||||
<kbd>{{ k }}</kbd>
|
||||
<span v-if="i < keys.length - 1">{{ combination }}</span>
|
||||
</template>
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
<template>
|
||||
<x-button
|
||||
v-if="type === 'button'"
|
||||
v-tooltip="tooltipText"
|
||||
variant="secondary"
|
||||
:icon="iconName"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:disabled="disabled"
|
||||
@click="changeSubscription"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
<DropdownItem
|
||||
v-else-if="type === 'dropdown'"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:disabled="disabled"
|
||||
:icon="iconName"
|
||||
@click="changeSubscription"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</DropdownItem>
|
||||
<BaseButton
|
||||
v-else
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:class="{'is-disabled': disabled}"
|
||||
:disabled="disabled"
|
||||
@click="changeSubscription"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="iconName"/>
|
||||
<icon :icon="iconName" />
|
||||
</span>
|
||||
{{ buttonText }}
|
||||
</BaseButton>
|
||||
|
@ -63,10 +63,10 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const subscriptionEntity = computed<string | null>(() => props.modelValue?.entity ?? null)
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const subscriptionEntity = computed<string | null>(() => props.modelValue?.entity ?? null)
|
||||
|
||||
const subscriptionService = shallowRef(new SubscriptionService())
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
|
|
@ -4,14 +4,17 @@
|
|||
:class="{'is-inline': isInline}"
|
||||
>
|
||||
<img
|
||||
v-tooltip="displayName"
|
||||
:height="avatarSize"
|
||||
:src="getAvatarUrl(user, avatarSize)"
|
||||
:width="avatarSize"
|
||||
:alt="'Avatar of ' + displayName"
|
||||
class="avatar"
|
||||
v-tooltip="displayName"
|
||||
/>
|
||||
<span class="username" v-if="showUsername">{{ displayName }}</span>
|
||||
>
|
||||
<span
|
||||
v-if="showUsername"
|
||||
class="username"
|
||||
>{{ displayName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,51 +1,80 @@
|
|||
<template>
|
||||
<div class="notifications">
|
||||
<slot name="trigger" toggleOpen="() => showNotifications = !showNotifications" :has-unread-notifications="unreadNotifications > 0">
|
||||
<BaseButton class="trigger-button" @click.stop="showNotifications = !showNotifications">
|
||||
<span class="unread-indicator" v-if="unreadNotifications > 0"></span>
|
||||
<icon icon="bell"/>
|
||||
<slot
|
||||
name="trigger"
|
||||
toggle-open="() => showNotifications = !showNotifications"
|
||||
:has-unread-notifications="unreadNotifications > 0"
|
||||
>
|
||||
<BaseButton
|
||||
class="trigger-button"
|
||||
@click.stop="showNotifications = !showNotifications"
|
||||
>
|
||||
<span
|
||||
v-if="unreadNotifications > 0"
|
||||
class="unread-indicator"
|
||||
/>
|
||||
<icon icon="bell" />
|
||||
</BaseButton>
|
||||
</slot>
|
||||
|
||||
<CustomTransition name="fade">
|
||||
<div class="notifications-list" v-if="showNotifications" ref="popup">
|
||||
<div
|
||||
v-if="showNotifications"
|
||||
ref="popup"
|
||||
class="notifications-list"
|
||||
>
|
||||
<span class="head">{{ $t('notification.title') }}</span>
|
||||
<div
|
||||
v-for="(n, index) in notifications"
|
||||
:key="n.id"
|
||||
class="single-notification"
|
||||
>
|
||||
<div class="read-indicator" :class="{'read': n.readAt !== null}"></div>
|
||||
<user
|
||||
<div
|
||||
class="read-indicator"
|
||||
:class="{'read': n.readAt !== null}"
|
||||
/>
|
||||
<User
|
||||
v-if="n.notification.doer"
|
||||
:user="n.notification.doer"
|
||||
:show-username="false"
|
||||
:avatar-size="16"
|
||||
v-if="n.notification.doer"
|
||||
/>
|
||||
<div class="detail">
|
||||
<div>
|
||||
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
|
||||
<span
|
||||
v-if="n.notification.doer"
|
||||
class="has-text-weight-bold mr-1"
|
||||
>
|
||||
{{ getDisplayName(n.notification.doer) }}
|
||||
</span>
|
||||
<BaseButton @click="() => to(n, index)()">
|
||||
<BaseButton
|
||||
class="has-text-left"
|
||||
@click="() => to(n, index)()"
|
||||
>
|
||||
{{ n.toText(userInfo) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<span class="created" v-tooltip="formatDateLong(n.created)">
|
||||
<span
|
||||
v-tooltip="formatDateLong(n.created)"
|
||||
class="created"
|
||||
>
|
||||
{{ formatDateSince(n.created) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<x-button
|
||||
<XButton
|
||||
v-if="notifications.length > 0 && unreadNotifications > 0"
|
||||
@click="markAllRead"
|
||||
variant="tertiary"
|
||||
class="mt-2 is-fullwidth"
|
||||
@click="markAllRead"
|
||||
>
|
||||
{{ $t('notification.markAllRead') }}
|
||||
</x-button>
|
||||
<p class="nothing" v-if="notifications.length === 0">
|
||||
{{ $t('notification.none') }}<br/>
|
||||
</XButton>
|
||||
<p
|
||||
v-if="notifications.length === 0"
|
||||
class="nothing"
|
||||
>
|
||||
{{ $t('notification.none') }}<br>
|
||||
<span class="explainer">
|
||||
{{ $t('notification.explainer') }}
|
||||
</span>
|
||||
|
@ -156,6 +185,8 @@ async function markAllRead() {
|
|||
const notificationService = new NotificationService()
|
||||
await notificationService.markAllRead()
|
||||
success({message: t('notification.markAllReadSuccess')})
|
||||
|
||||
notifications.value.forEach(n => n.readAt = new Date())
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -223,6 +254,7 @@ async function markAllRead() {
|
|||
background: var(--primary);
|
||||
border-radius: 100%;
|
||||
margin: 0 .5rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.read {
|
||||
background: transparent;
|
||||
|
|
|
@ -49,12 +49,16 @@
|
|||
<slot name="header" />
|
||||
</div>
|
||||
<CustomTransition name="fade">
|
||||
<Message variant="warning" v-if="currentProject?.isArchived" class="mb-4">
|
||||
<Message
|
||||
v-if="currentProject?.isArchived"
|
||||
variant="warning"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ $t('project.archivedMessage') }}
|
||||
</Message>
|
||||
</CustomTransition>
|
||||
|
||||
<slot v-if="loadedProjectId"/>
|
||||
<slot v-if="loadedProjectId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -15,11 +15,20 @@
|
|||
:class="{'is-visible': background}"
|
||||
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||
/>
|
||||
<span v-if="project.isArchived" class="is-archived" >{{ $t('project.archived') }}</span>
|
||||
<span
|
||||
v-if="project.isArchived"
|
||||
class="is-archived"
|
||||
>{{ $t('project.archived') }}</span>
|
||||
|
||||
<div class="project-title" aria-hidden="true">
|
||||
<span v-if="project.id < -1" class="saved-filter-icon icon">
|
||||
<icon icon="filter"/>
|
||||
<div
|
||||
class="project-title"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
v-if="project.id < -1"
|
||||
class="saved-filter-icon icon"
|
||||
>
|
||||
<icon icon="filter" />
|
||||
</span>
|
||||
{{ project.title }}
|
||||
</div>
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
{{ $t('filters.clear') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="() => modalOpen = true"
|
||||
variant="secondary"
|
||||
icon="filter"
|
||||
@click="() => modalOpen = true"
|
||||
>
|
||||
{{ $t('filters.title') }}
|
||||
</x-button>
|
||||
|
@ -20,10 +20,10 @@
|
|||
variant="hint-modal"
|
||||
@close="() => modalOpen = false"
|
||||
>
|
||||
<filters
|
||||
:has-title="true"
|
||||
v-model="value"
|
||||
<Filters
|
||||
ref="filters"
|
||||
v-model="value"
|
||||
:has-title="true"
|
||||
class="filter-popup"
|
||||
/>
|
||||
</modal>
|
||||
|
@ -48,6 +48,9 @@ const value = computed({
|
|||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
if(props.modelValue === value) {
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,134 +1,157 @@
|
|||
<template>
|
||||
<card class="filters has-overflow" :title="hasTitle ? $t('filters.title') : ''">
|
||||
<card
|
||||
class="filters has-overflow"
|
||||
:title="hasTitle ? $t('filters.title') : ''"
|
||||
>
|
||||
<div class="field is-flex is-flex-direction-column">
|
||||
<fancycheckbox
|
||||
<Fancycheckbox
|
||||
v-model="params.filter_include_nulls"
|
||||
@update:model-value="change()"
|
||||
@update:modelValue="change()"
|
||||
>
|
||||
{{ $t('filters.attributes.includeNulls') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox
|
||||
v-model="filters.requireAllFilters"
|
||||
@update:model-value="setFilterConcat()"
|
||||
@update:modelValue="setFilterConcat()"
|
||||
>
|
||||
{{ $t('filters.attributes.requireAll') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox
|
||||
v-model="filters.done"
|
||||
@update:model-value="setDoneFilter"
|
||||
@update:modelValue="setDoneFilter"
|
||||
>
|
||||
{{ $t('filters.attributes.showDoneTasks') }}
|
||||
</fancycheckbox>
|
||||
<fancycheckbox
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox
|
||||
v-if="!['project.kanban', 'project.table'].includes($route.name as string)"
|
||||
v-model="sortAlphabetically"
|
||||
@update:model-value="change()"
|
||||
@update:modelValue="change()"
|
||||
>
|
||||
{{ $t('filters.attributes.sortAlphabetically') }}
|
||||
</fancycheckbox>
|
||||
</Fancycheckbox>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('misc.search') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
v-model="params.s"
|
||||
class="input"
|
||||
:placeholder="$t('misc.search')"
|
||||
v-model="params.s"
|
||||
@blur="change()"
|
||||
@keyup.enter="change()"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.priority') }}</label>
|
||||
<div class="control single-value-control">
|
||||
<priority-select
|
||||
<PrioritySelect
|
||||
v-model.number="filters.priority"
|
||||
@update:model-value="setPriority"
|
||||
:disabled="!filters.usePriority || undefined"
|
||||
@update:modelValue="setPriority"
|
||||
/>
|
||||
<fancycheckbox
|
||||
<Fancycheckbox
|
||||
v-model="filters.usePriority"
|
||||
@update:model-value="setPriority"
|
||||
@update:modelValue="setPriority"
|
||||
>
|
||||
{{ $t('filters.attributes.enablePriority') }}
|
||||
</fancycheckbox>
|
||||
</Fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.percentDone') }}</label>
|
||||
<div class="control single-value-control">
|
||||
<percent-done-select
|
||||
<PercentDoneSelect
|
||||
v-model.number="filters.percentDone"
|
||||
@update:model-value="setPercentDoneFilter"
|
||||
:disabled="!filters.usePercentDone || undefined"
|
||||
@update:modelValue="setPercentDoneFilter"
|
||||
/>
|
||||
<fancycheckbox
|
||||
<Fancycheckbox
|
||||
v-model="filters.usePercentDone"
|
||||
@update:model-value="setPercentDoneFilter"
|
||||
@update:modelValue="setPercentDoneFilter"
|
||||
>
|
||||
{{ $t('filters.attributes.enablePercentDone') }}
|
||||
</fancycheckbox>
|
||||
</Fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.dueDate') }}</label>
|
||||
<div class="control">
|
||||
<datepicker-with-range
|
||||
<DatepickerWithRange
|
||||
v-model="filters.dueDate"
|
||||
@update:model-value="values => setDateFilter('due_date', values)"
|
||||
@update:modelValue="values => setDateFilter('due_date', values)"
|
||||
>
|
||||
<template #trigger="{toggle, buttonText}">
|
||||
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
|
||||
<x-button
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
class="mb-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
</template>
|
||||
</datepicker-with-range>
|
||||
</DatepickerWithRange>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.startDate') }}</label>
|
||||
<div class="control">
|
||||
<datepicker-with-range
|
||||
<DatepickerWithRange
|
||||
v-model="filters.startDate"
|
||||
@update:model-value="values => setDateFilter('start_date', values)"
|
||||
@update:modelValue="values => setDateFilter('start_date', values)"
|
||||
>
|
||||
<template #trigger="{toggle, buttonText}">
|
||||
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
|
||||
<x-button
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
class="mb-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
</template>
|
||||
</datepicker-with-range>
|
||||
</DatepickerWithRange>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.endDate') }}</label>
|
||||
<div class="control">
|
||||
<datepicker-with-range
|
||||
<DatepickerWithRange
|
||||
v-model="filters.endDate"
|
||||
@update:model-value="values => setDateFilter('end_date', values)"
|
||||
@update:modelValue="values => setDateFilter('end_date', values)"
|
||||
>
|
||||
<template #trigger="{toggle, buttonText}">
|
||||
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
|
||||
<x-button
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
class="mb-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
</template>
|
||||
</datepicker-with-range>
|
||||
</DatepickerWithRange>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.reminders') }}</label>
|
||||
<div class="control">
|
||||
<datepicker-with-range
|
||||
<DatepickerWithRange
|
||||
v-model="filters.reminders"
|
||||
@update:model-value="values => setDateFilter('reminders', values)"
|
||||
@update:modelValue="values => setDateFilter('reminders', values)"
|
||||
>
|
||||
<template #trigger="{toggle, buttonText}">
|
||||
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
|
||||
<x-button
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
class="mb-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
</template>
|
||||
</datepicker-with-range>
|
||||
</DatepickerWithRange>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -146,10 +169,10 @@
|
|||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.labels') }}</label>
|
||||
<div class="control labels-list">
|
||||
<edit-labels
|
||||
:creatable="false"
|
||||
<EditLabels
|
||||
v-model="entities.labels"
|
||||
@update:model-value="changeLabelFilter"
|
||||
:creatable="false"
|
||||
@update:modelValue="changeLabelFilter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -162,9 +185,9 @@
|
|||
<div class="control">
|
||||
<SelectProject
|
||||
v-model="entities.projects"
|
||||
:project-filter="p => p.id > 0"
|
||||
@select="changeMultiselectFilter('projects', 'project_id')"
|
||||
@remove="changeMultiselectFilter('projects', 'project_id')"
|
||||
:project-filter="p => p.id > 0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -205,6 +228,18 @@ import ProjectService from '@/services/project'
|
|||
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
hasTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskProject.js
|
||||
const DEFAULT_PARAMS = {
|
||||
sort_by: [],
|
||||
|
@ -233,18 +268,6 @@ const DEFAULT_FILTERS = {
|
|||
project_id: '',
|
||||
} as const
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
hasTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const {modelValue} = toRefs(props)
|
||||
|
||||
const labelStore = useLabelStore()
|
||||
|
|
|
@ -1,99 +1,108 @@
|
|||
<template>
|
||||
<dropdown>
|
||||
<Dropdown>
|
||||
<template #trigger="triggerProps">
|
||||
<slot name="trigger" v-bind="triggerProps">
|
||||
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
<slot
|
||||
name="trigger"
|
||||
v-bind="triggerProps"
|
||||
>
|
||||
<BaseButton
|
||||
class="dropdown-trigger"
|
||||
@click="triggerProps.toggleOpen"
|
||||
>
|
||||
<icon
|
||||
icon="ellipsis-h"
|
||||
class="icon"
|
||||
/>
|
||||
</BaseButton>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-if="isSavedFilter(project)">
|
||||
<dropdown-item
|
||||
<DropdownItem
|
||||
:to="{ name: 'filter.settings.edit', params: { projectId: project.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
:to="{ name: 'filter.settings.delete', params: { projectId: project.id } }"
|
||||
icon="trash-alt"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
</dropdown-item>
|
||||
</DropdownItem>
|
||||
</template>
|
||||
|
||||
<template v-else-if="project.isArchived">
|
||||
<dropdown-item
|
||||
<DropdownItem
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
</dropdown-item>
|
||||
</DropdownItem>
|
||||
</template>
|
||||
<template v-else>
|
||||
<dropdown-item
|
||||
<DropdownItem
|
||||
:to="{ name: 'project.settings.edit', params: { projectId: project.id } }"
|
||||
icon="pen"
|
||||
>
|
||||
{{ $t('menu.edit') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-if="backgroundsEnabled"
|
||||
:to="{ name: 'project.settings.background', params: { projectId: project.id } }"
|
||||
icon="image"
|
||||
>
|
||||
{{ $t('menu.setBackground') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
:to="{ name: 'project.settings.share', params: { projectId: project.id } }"
|
||||
icon="share-alt"
|
||||
>
|
||||
{{ $t('menu.share') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
:to="{ name: 'project.settings.duplicate', params: { projectId: project.id } }"
|
||||
icon="paste"
|
||||
>
|
||||
{{ $t('menu.duplicate') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
icon="archive"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
</dropdown-item>
|
||||
</DropdownItem>
|
||||
<Subscription
|
||||
class="has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="project"
|
||||
:entity-id="project.id"
|
||||
:model-value="project.subscription"
|
||||
@update:model-value="setSubscriptionInStore"
|
||||
type="dropdown"
|
||||
@update:modelValue="setSubscriptionInStore"
|
||||
/>
|
||||
<dropdown-item
|
||||
<DropdownItem
|
||||
:to="{ name: 'project.settings.webhooks', params: { projectId: project.id } }"
|
||||
icon="bolt"
|
||||
>
|
||||
{{ $t('project.webhooks.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-if="level < 2"
|
||||
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
|
||||
icon="layer-group"
|
||||
>
|
||||
{{ $t('menu.createProject') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
:to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
>
|
||||
{{ $t('menu.delete') }}
|
||||
</dropdown-item>
|
||||
</DropdownItem>
|
||||
</template>
|
||||
</dropdown>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -1,33 +1,53 @@
|
|||
<template>
|
||||
<modal :enabled="active" @close="closeQuickActions" :overflow="isNewTaskCommand">
|
||||
<modal
|
||||
:enabled="active"
|
||||
:overflow="isNewTaskCommand"
|
||||
@close="closeQuickActions"
|
||||
>
|
||||
<div class="card quick-actions">
|
||||
<div class="action-input" :class="{'has-active-cmd': selectedCmd !== null}">
|
||||
<div class="active-cmd tag" v-if="selectedCmd !== null">
|
||||
<div
|
||||
class="action-input"
|
||||
:class="{'has-active-cmd': selectedCmd !== null}"
|
||||
>
|
||||
<div
|
||||
v-if="selectedCmd !== null"
|
||||
class="active-cmd tag"
|
||||
>
|
||||
{{ selectedCmd.title }}
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="query"
|
||||
v-focus
|
||||
class="input"
|
||||
:class="{'is-loading': loading}"
|
||||
v-model="query"
|
||||
:placeholder="placeholder"
|
||||
@keyup="search"
|
||||
ref="searchInput"
|
||||
@keydown.down.prevent="select(0, 0)"
|
||||
@keyup.prevent.delete="unselectCmd"
|
||||
@keyup.prevent.enter="doCmd"
|
||||
@keyup.prevent.esc="closeQuickActions"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="help has-text-grey-light p-2" v-if="hintText !== '' && !isNewTaskCommand">
|
||||
<div
|
||||
v-if="hintText !== '' && !isNewTaskCommand"
|
||||
class="help has-text-grey-light p-2"
|
||||
>
|
||||
{{ hintText }}
|
||||
</div>
|
||||
|
||||
<quick-add-magic v-if="isNewTaskCommand"/>
|
||||
<QuickAddMagic v-if="isNewTaskCommand" />
|
||||
|
||||
<div class="results" v-if="selectedCmd === null">
|
||||
<div v-for="(r, k) in results" :key="k" class="result">
|
||||
<div
|
||||
v-if="selectedCmd === null"
|
||||
class="results"
|
||||
>
|
||||
<div
|
||||
v-for="(r, k) in results"
|
||||
:key="k"
|
||||
class="result"
|
||||
>
|
||||
<span class="result-title">
|
||||
{{ r.title }}
|
||||
</span>
|
||||
|
@ -35,9 +55,9 @@
|
|||
<BaseButton
|
||||
v-for="(i, key) in r.items"
|
||||
:key="key"
|
||||
:ref="(el: Element | ComponentPublicInstance | null) => setResultRefs(el, k, key)"
|
||||
class="result-item-button"
|
||||
:class="{'is-strikethrough': (i as DoAction<ITask>)?.done}"
|
||||
:ref="(el: Element | ComponentPublicInstance | null) => setResultRefs(el, k, key)"
|
||||
@keydown.up.prevent="select(k, key - 1)"
|
||||
@keydown.down.prevent="select(k, key + 1)"
|
||||
@click.prevent.stop="doAction(r.type, i)"
|
||||
|
@ -45,10 +65,10 @@
|
|||
@keyup.prevent.esc="searchInput?.focus()"
|
||||
>
|
||||
<template v-if="r.type === ACTION_TYPE.LABELS">
|
||||
<x-label :label="i"/>
|
||||
<XLabel :label="i" />
|
||||
</template>
|
||||
<template v-else-if="r.type === ACTION_TYPE.TASK">
|
||||
<single-task-inline-readonly
|
||||
<SingleTaskInlineReadonly
|
||||
:task="i"
|
||||
:show-project="true"
|
||||
/>
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
<p class="has-text-weight-bold">
|
||||
{{ $t('project.share.links.title') }}
|
||||
<span
|
||||
v-tooltip="$t('project.share.links.explanation')"
|
||||
class="is-size-7 has-text-grey is-italic ml-3"
|
||||
v-tooltip="$t('project.share.links.explanation')">
|
||||
>
|
||||
{{ $t('project.share.links.what') }}
|
||||
</span>
|
||||
</p>
|
||||
|
@ -12,20 +13,30 @@
|
|||
<div class="sharables-project">
|
||||
<x-button
|
||||
v-if="!(linkShares.length === 0 || showNewForm)"
|
||||
@click="showNewForm = true"
|
||||
icon="plus"
|
||||
class="mb-4">
|
||||
class="mb-4"
|
||||
@click="showNewForm = true"
|
||||
>
|
||||
{{ $t('project.share.links.create') }}
|
||||
</x-button>
|
||||
|
||||
<div class="p-4" v-if="linkShares.length === 0 || showNewForm">
|
||||
<div
|
||||
v-if="linkShares.length === 0 || showNewForm"
|
||||
class="p-4"
|
||||
>
|
||||
<div class="field">
|
||||
<label class="label" for="linkShareRight">
|
||||
<label
|
||||
class="label"
|
||||
for="linkShareRight"
|
||||
>
|
||||
{{ $t('project.share.right.title') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select v-model="selectedRight" id="linkShareRight">
|
||||
<select
|
||||
id="linkShareRight"
|
||||
v-model="selectedRight"
|
||||
>
|
||||
<option :value="RIGHTS.READ">
|
||||
{{ $t('project.share.right.read') }}
|
||||
</option>
|
||||
|
@ -40,59 +51,77 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="linkShareName">
|
||||
<label
|
||||
class="label"
|
||||
for="linkShareName"
|
||||
>
|
||||
{{ $t('project.share.links.name') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="linkShareName"
|
||||
v-model="name"
|
||||
v-tooltip="$t('project.share.links.nameExplanation')"
|
||||
class="input"
|
||||
:placeholder="$t('project.share.links.namePlaceholder')"
|
||||
v-tooltip="$t('project.share.links.nameExplanation')"
|
||||
v-model="name"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="linkSharePassword">
|
||||
<label
|
||||
class="label"
|
||||
for="linkSharePassword"
|
||||
>
|
||||
{{ $t('project.share.links.password') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="linkSharePassword"
|
||||
v-model="password"
|
||||
v-tooltip="$t('project.share.links.passwordExplanation')"
|
||||
type="password"
|
||||
class="input"
|
||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
v-tooltip="$t('project.share.links.passwordExplanation')"
|
||||
v-model="password"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<x-button @click="add(projectId)" icon="plus">
|
||||
<x-button
|
||||
icon="plus"
|
||||
@click="add(projectId)"
|
||||
>
|
||||
{{ $t('project.share.share') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
||||
<table
|
||||
class="table has-actions is-striped is-hoverable is-fullwidth"
|
||||
v-if="linkShares.length > 0"
|
||||
class="table has-actions is-striped is-hoverable is-fullwidth"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th />
|
||||
<th>{{ $t('project.share.links.view') }}</th>
|
||||
<th>{{ $t('project.share.attributes.delete') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :key="s.id" v-for="s in linkShares">
|
||||
<tr
|
||||
v-for="s in linkShares"
|
||||
:key="s.id"
|
||||
>
|
||||
<td>
|
||||
<p class="mb-2 is-italic" v-if="s.name !== ''">
|
||||
<p
|
||||
v-if="s.name !== ''"
|
||||
class="mb-2 is-italic"
|
||||
>
|
||||
{{ s.name }}
|
||||
</p>
|
||||
|
||||
<p class="mb-2">
|
||||
<i18n-t keypath="project.share.links.sharedBy" scope="global">
|
||||
<i18n-t
|
||||
keypath="project.share.links.sharedBy"
|
||||
scope="global"
|
||||
>
|
||||
<strong>{{ getDisplayName(s.sharedBy) }}</strong>
|
||||
</i18n-t>
|
||||
</p>
|
||||
|
@ -100,19 +129,19 @@
|
|||
<p class="mb-2">
|
||||
<template v-if="s.right === RIGHTS.ADMIN">
|
||||
<span class="icon is-small">
|
||||
<icon icon="lock"/>
|
||||
<icon icon="lock" />
|
||||
</span>
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</template>
|
||||
<template v-else-if="s.right === RIGHTS.READ_WRITE">
|
||||
<span class="icon is-small">
|
||||
<icon icon="pen"/>
|
||||
<icon icon="pen" />
|
||||
</span>
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="icon is-small">
|
||||
<icon icon="users"/>
|
||||
<icon icon="users" />
|
||||
</span>
|
||||
{{ $t('project.share.right.read') }}
|
||||
</template>
|
||||
|
@ -125,16 +154,16 @@
|
|||
class="input"
|
||||
readonly
|
||||
type="text"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button
|
||||
@click="copy(getShareLink(s.hash, selectedView[s.id]))"
|
||||
:shadow="false"
|
||||
v-tooltip="$t('misc.copy')"
|
||||
:shadow="false"
|
||||
@click="copy(getShareLink(s.hash, selectedView[s.id]))"
|
||||
>
|
||||
<span class="icon">
|
||||
<icon icon="paste"/>
|
||||
<icon icon="paste" />
|
||||
</span>
|
||||
</x-button>
|
||||
</div>
|
||||
|
@ -145,8 +174,9 @@
|
|||
<select v-model="selectedView[s.id]">
|
||||
<option
|
||||
v-for="(title, key) in availableViews"
|
||||
:key="key"
|
||||
:value="key"
|
||||
:key="key">
|
||||
>
|
||||
{{ title }}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -154,14 +184,14 @@
|
|||
</td>
|
||||
<td class="actions">
|
||||
<x-button
|
||||
class="is-danger"
|
||||
icon="trash-alt"
|
||||
@click="
|
||||
() => {
|
||||
linkIdToDelete = s.id
|
||||
showDeleteModal = true
|
||||
}
|
||||
"
|
||||
class="is-danger"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -207,7 +237,7 @@ import {useConfigStore} from '@/stores/config'
|
|||
const props = defineProps({
|
||||
projectId: {
|
||||
default: 0,
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -10,23 +10,31 @@
|
|||
:class="{ 'is-loading': searchService.loading }"
|
||||
>
|
||||
<Multiselect
|
||||
v-model="sharable"
|
||||
:loading="searchService.loading"
|
||||
:placeholder="$t('misc.searchPlaceholder')"
|
||||
@search="find"
|
||||
:search-results="found"
|
||||
:label="searchLabel"
|
||||
v-model="sharable"
|
||||
@search="find"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<x-button @click="add()">{{ $t('project.share.share') }}</x-button>
|
||||
<x-button @click="add()">
|
||||
{{ $t('project.share.share') }}
|
||||
</x-button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table has-actions is-striped is-hoverable is-fullwidth mb-4" v-if="sharables.length > 0">
|
||||
<table
|
||||
v-if="sharables.length > 0"
|
||||
class="table has-actions is-striped is-hoverable is-fullwidth mb-4"
|
||||
>
|
||||
<tbody>
|
||||
<tr :key="s.id" v-for="s in sharables">
|
||||
<tr
|
||||
v-for="s in sharables"
|
||||
:key="s.id"
|
||||
>
|
||||
<template v-if="shareType === 'user'">
|
||||
<td>{{ getDisplayName(s) }}</td>
|
||||
<td>
|
||||
|
@ -50,29 +58,32 @@
|
|||
<td class="type">
|
||||
<template v-if="s.right === RIGHTS.ADMIN">
|
||||
<span class="icon is-small">
|
||||
<icon icon="lock"/>
|
||||
<icon icon="lock" />
|
||||
</span>
|
||||
{{ $t('project.share.right.admin') }}
|
||||
</template>
|
||||
<template v-else-if="s.right === RIGHTS.READ_WRITE">
|
||||
<span class="icon is-small">
|
||||
<icon icon="pen"/>
|
||||
<icon icon="pen" />
|
||||
</span>
|
||||
{{ $t('project.share.right.readWrite') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="icon is-small">
|
||||
<icon icon="users"/>
|
||||
<icon icon="users" />
|
||||
</span>
|
||||
{{ $t('project.share.right.read') }}
|
||||
</template>
|
||||
</td>
|
||||
<td class="actions" v-if="userIsAdmin">
|
||||
<td
|
||||
v-if="userIsAdmin"
|
||||
class="actions"
|
||||
>
|
||||
<div class="select">
|
||||
<select
|
||||
@change="toggleType(s)"
|
||||
class="mr-2"
|
||||
v-model="selectedRight[s.id]"
|
||||
class="mr-2"
|
||||
@change="toggleType(s)"
|
||||
>
|
||||
<option
|
||||
:selected="s.right === RIGHTS.READ"
|
||||
|
@ -95,23 +106,23 @@
|
|||
</select>
|
||||
</div>
|
||||
<x-button
|
||||
class="is-danger"
|
||||
icon="trash-alt"
|
||||
@click="
|
||||
() => {
|
||||
sharable = s
|
||||
showDeleteModal = true
|
||||
}
|
||||
"
|
||||
class="is-danger"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<nothing v-else>
|
||||
<Nothing v-else>
|
||||
{{ $t('project.share.userTeam.notShared', {type: shareTypeNames}) }}
|
||||
</nothing>
|
||||
</Nothing>
|
||||
|
||||
<modal
|
||||
:enabled="showDeleteModal"
|
||||
|
@ -131,7 +142,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {name: 'userTeamShare'}
|
||||
export default {name: 'UserTeamShare'}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
v-if="props.isLoading && !ganttBars.length || dayjsLanguageLoading"
|
||||
class="gantt-container"
|
||||
/>
|
||||
<div ref="ganttContainer" class="gantt-container" v-else>
|
||||
<div
|
||||
v-else
|
||||
ref="ganttContainer"
|
||||
class="gantt-container"
|
||||
>
|
||||
<GGanttChart
|
||||
:date-format="DAYJS_ISO_DATE_FORMAT"
|
||||
:chart-start="isoToKebabDate(filters.dateFrom)"
|
||||
|
@ -12,9 +16,9 @@
|
|||
bar-start="startDate"
|
||||
bar-end="endDate"
|
||||
:grid="true"
|
||||
@dragend-bar="updateGanttTask"
|
||||
@dblclick-bar="openTask"
|
||||
:width="ganttChartWidth"
|
||||
@dragendBar="updateGanttTask"
|
||||
@dblclickBar="openTask"
|
||||
>
|
||||
<template #timeunit="{value, date}">
|
||||
<div
|
||||
|
@ -61,6 +65,8 @@ import {
|
|||
import Loading from '@/components/misc/loading.vue'
|
||||
import {MILLISECONDS_A_DAY} from '@/constants/date'
|
||||
import {useWeekDayFromDate} from '@/helpers/time/formatDate'
|
||||
import dayjs from 'dayjs'
|
||||
import {useDayjsLanguageSync} from '@/i18n/useDayjsLanguageSync'
|
||||
|
||||
export interface GanttChartProps {
|
||||
isLoading: boolean,
|
||||
|
@ -70,19 +76,19 @@ export interface GanttChartProps {
|
|||
defaultTaskEndDate: DateISO
|
||||
}
|
||||
|
||||
const DAYJS_ISO_DATE_FORMAT = 'YYYY-MM-DD'
|
||||
|
||||
const props = defineProps<GanttChartProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:task', task: ITaskPartialWithId): void
|
||||
}>()
|
||||
|
||||
const DAYJS_ISO_DATE_FORMAT = 'YYYY-MM-DD'
|
||||
|
||||
const {tasks, filters} = toRefs(props)
|
||||
|
||||
// setup dayjs for vue-ganttastic
|
||||
const dayjsLanguageLoading = ref(false)
|
||||
// const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
|
||||
// const dayjsLanguageLoading = ref(false)
|
||||
const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
|
||||
extendDayjs()
|
||||
|
||||
const ganttContainer = ref(null)
|
||||
|
@ -121,6 +127,23 @@ watch(
|
|||
|
||||
function transformTaskToGanttBar(t: ITask) {
|
||||
const black = 'var(--grey-800)'
|
||||
|
||||
const taskColor = getHexColor(t.hexColor)
|
||||
|
||||
let textColor = black
|
||||
let backgroundColor = 'var(--grey-100)'
|
||||
if(t.startDate) {
|
||||
backgroundColor = taskColor ?? ''
|
||||
if(typeof taskColor === 'undefined') {
|
||||
textColor = 'white'
|
||||
backgroundColor = 'var(--primary)'
|
||||
} else if(colorIsDark(taskColor)) {
|
||||
textColor = black
|
||||
} else {
|
||||
textColor = 'white'
|
||||
}
|
||||
}
|
||||
|
||||
return [{
|
||||
startDate: isoToKebabDate(t.startDate ? t.startDate.toISOString() : props.defaultTaskStartDate),
|
||||
endDate: isoToKebabDate(t.endDate ? t.endDate.toISOString() : props.defaultTaskEndDate),
|
||||
|
@ -129,8 +152,8 @@ function transformTaskToGanttBar(t: ITask) {
|
|||
label: t.title,
|
||||
hasHandles: true,
|
||||
style: {
|
||||
color: t.startDate ? (colorIsDark(getHexColor(t.hexColor)) ? black : 'white') : black,
|
||||
backgroundColor: t.startDate ? getHexColor(t.hexColor) : 'var(--grey-100)',
|
||||
color: textColor,
|
||||
backgroundColor,
|
||||
border: t.startDate ? '' : '2px dashed var(--grey-300)',
|
||||
'text-decoration': t.done ? 'line-through' : null,
|
||||
},
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
<template>
|
||||
<form
|
||||
@submit.prevent="createTask"
|
||||
class="add-new-task"
|
||||
@submit.prevent="createTask"
|
||||
>
|
||||
<CustomTransition name="width">
|
||||
<input
|
||||
v-if="newTaskFieldActive"
|
||||
ref="newTaskTitleField"
|
||||
v-model="newTaskTitle"
|
||||
class="input"
|
||||
type="text"
|
||||
@blur="hideCreateNewTask"
|
||||
@keyup.esc="newTaskFieldActive = false"
|
||||
class="input"
|
||||
ref="newTaskTitleField"
|
||||
type="text"
|
||||
/>
|
||||
>
|
||||
</CustomTransition>
|
||||
<x-button @click="showCreateTaskOrCreate" :shadow="false" icon="plus">
|
||||
<x-button
|
||||
:shadow="false"
|
||||
icon="plus"
|
||||
@click="showCreateTaskOrCreate"
|
||||
>
|
||||
{{ $t('task.new') }}
|
||||
</x-button>
|
||||
</form>
|
||||
|
@ -27,7 +31,7 @@ import type {ITask} from '@/modelTypes/ITask'
|
|||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create-task', title: string): Promise<ITask>
|
||||
(e: 'createTask', title: string): Promise<ITask>
|
||||
}>()
|
||||
|
||||
const newTaskFieldActive = ref(false)
|
||||
|
@ -56,7 +60,7 @@ async function createTask() {
|
|||
if (!newTaskFieldActive.value) {
|
||||
return
|
||||
}
|
||||
await emit('create-task', newTaskTitle.value)
|
||||
await emit('createTask', newTaskTitle.value)
|
||||
newTaskTitle.value = ''
|
||||
hideCreateNewTask()
|
||||
}
|
||||
|
|
|
@ -1,31 +1,34 @@
|
|||
<template>
|
||||
<div class="task-add" ref="taskAdd">
|
||||
<div
|
||||
ref="taskAdd"
|
||||
class="task-add"
|
||||
>
|
||||
<div class="add-task__field field is-grouped">
|
||||
<p class="control has-icons-left has-icons-right is-expanded">
|
||||
<textarea
|
||||
ref="newTaskInput"
|
||||
v-model="newTaskTitle"
|
||||
v-focus
|
||||
class="add-task-textarea input"
|
||||
:class="{'textarea-empty': newTaskTitle === ''}"
|
||||
:placeholder="$t('project.list.addPlaceholder')"
|
||||
rows="1"
|
||||
v-focus
|
||||
v-model="newTaskTitle"
|
||||
ref="newTaskInput"
|
||||
@keyup="resetEmptyTitleError"
|
||||
@keydown.enter="handleEnter"
|
||||
/>
|
||||
<span class="icon is-small is-left">
|
||||
<icon icon="tasks"/>
|
||||
<icon icon="tasks" />
|
||||
</span>
|
||||
<quick-add-magic :highlight-hint-icon="taskAddHovered"/>
|
||||
<QuickAddMagic :highlight-hint-icon="taskAddHovered" />
|
||||
</p>
|
||||
<p class="control">
|
||||
<x-button
|
||||
class="add-task-button"
|
||||
:disabled="newTaskTitle === '' || loading || undefined"
|
||||
@click="addTask()"
|
||||
icon="plus"
|
||||
:loading="loading"
|
||||
:aria-label="$t('project.list.add')"
|
||||
@click="addTask()"
|
||||
>
|
||||
<span class="button-text">
|
||||
{{ $t('project.list.add') }}
|
||||
|
@ -34,7 +37,10 @@
|
|||
</p>
|
||||
</div>
|
||||
<Expandable :open="errorMessage !== ''">
|
||||
<p class="pt-3 mt-0 help is-danger" v-if="errorMessage !== ''">
|
||||
<p
|
||||
v-if="errorMessage !== ''"
|
||||
class="pt-3 mt-0 help is-danger"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</Expandable>
|
||||
|
|
|
@ -22,26 +22,29 @@ const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="assignees-list" :class="{'is-inline': inline}">
|
||||
<div
|
||||
class="assignees-list"
|
||||
:class="{'is-inline': inline}"
|
||||
>
|
||||
<span
|
||||
v-for="user in assignees"
|
||||
class="assignee"
|
||||
:key="user.id"
|
||||
class="assignee"
|
||||
>
|
||||
<User
|
||||
:key="'user'+user.id"
|
||||
:avatar-size="avatarSize"
|
||||
:show-username="false"
|
||||
:user="user"
|
||||
:class="{'m-2': hasDelete, 'mr-3': !hasDelete}"
|
||||
:class="{'m-2': hasDelete}"
|
||||
/>
|
||||
<BaseButton
|
||||
:key="'delete'+user.id"
|
||||
v-if="hasDelete"
|
||||
@click="remove(user)"
|
||||
:key="'delete'+user.id"
|
||||
class="remove-assignee"
|
||||
@click="remove(user)"
|
||||
>
|
||||
<icon icon="times"/>
|
||||
<icon icon="times" />
|
||||
</BaseButton>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -2,37 +2,38 @@
|
|||
<div class="attachments">
|
||||
<h3>
|
||||
<span class="icon is-grey">
|
||||
<icon icon="paperclip"/>
|
||||
<icon icon="paperclip" />
|
||||
</span>
|
||||
{{ $t('task.attachment.title') }}
|
||||
</h3>
|
||||
|
||||
<input
|
||||
v-if="editEnabled"
|
||||
:disabled="loading || undefined"
|
||||
@change="uploadNewAttachment()"
|
||||
id="files"
|
||||
multiple
|
||||
ref="filesRef"
|
||||
:disabled="loading || undefined"
|
||||
multiple
|
||||
type="file"
|
||||
/>
|
||||
<progress
|
||||
v-if="attachmentService.uploadProgress > 0"
|
||||
:value="attachmentService.uploadProgress"
|
||||
class="progress is-primary"
|
||||
max="100"
|
||||
@change="uploadNewAttachment()"
|
||||
>
|
||||
{{ attachmentService.uploadProgress }}%
|
||||
</progress>
|
||||
|
||||
<div class="files" v-if="attachments.length > 0">
|
||||
<ProgressBar
|
||||
v-if="attachmentService.uploadProgress > 0"
|
||||
:value="attachmentService.uploadProgress * 100"
|
||||
is-primary
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="attachments.length > 0"
|
||||
class="files"
|
||||
>
|
||||
<!-- FIXME: don't use a for element that wraps other links / buttons
|
||||
Instead: overlay element with button that is inside.
|
||||
-->
|
||||
<a
|
||||
class="attachment"
|
||||
v-for="a in attachments"
|
||||
:key="a.id"
|
||||
class="attachment"
|
||||
@click="viewOrDownload(a)"
|
||||
>
|
||||
<div class="filename">
|
||||
|
@ -46,7 +47,10 @@
|
|||
</div>
|
||||
<div class="info">
|
||||
<p class="attachment-info-meta">
|
||||
<i18n-t keypath="task.attachment.createdBy" scope="global">
|
||||
<i18n-t
|
||||
keypath="task.attachment.createdBy"
|
||||
scope="global"
|
||||
>
|
||||
<span v-tooltip="formatDateLong(a.created)">
|
||||
{{ formatDateSince(a.created) }}
|
||||
</span>
|
||||
|
@ -65,24 +69,24 @@
|
|||
</p>
|
||||
<p>
|
||||
<BaseButton
|
||||
v-tooltip="$t('task.attachment.downloadTooltip')"
|
||||
class="attachment-info-meta-button"
|
||||
@click.prevent.stop="downloadAttachment(a)"
|
||||
v-tooltip="$t('task.attachment.downloadTooltip')"
|
||||
>
|
||||
{{ $t('misc.download') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-tooltip="$t('task.attachment.copyUrlTooltip')"
|
||||
class="attachment-info-meta-button"
|
||||
@click.stop="copyUrl(a)"
|
||||
v-tooltip="$t('task.attachment.copyUrlTooltip')"
|
||||
>
|
||||
{{ $t('task.attachment.copyUrl') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="editEnabled"
|
||||
v-tooltip="$t('task.attachment.deleteTooltip')"
|
||||
class="attachment-info-meta-button"
|
||||
@click.prevent.stop="setAttachmentToDelete(a)"
|
||||
v-tooltip="$t('task.attachment.deleteTooltip')"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
</BaseButton>
|
||||
|
@ -105,11 +109,11 @@
|
|||
<x-button
|
||||
v-if="editEnabled"
|
||||
:disabled="loading"
|
||||
@click="filesRef?.click()"
|
||||
class="mb-4"
|
||||
icon="cloud-upload-alt"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
@click="filesRef?.click()"
|
||||
>
|
||||
{{ $t('task.attachment.upload') }}
|
||||
</x-button>
|
||||
|
@ -117,15 +121,17 @@
|
|||
<!-- Dropzone -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="editEnabled"
|
||||
:class="{ hidden: !isOverDropZone }"
|
||||
class="dropzone"
|
||||
v-if="editEnabled"
|
||||
>
|
||||
<div class="drop-hint">
|
||||
<div class="icon">
|
||||
<icon icon="cloud-upload-alt"/>
|
||||
<icon icon="cloud-upload-alt" />
|
||||
</div>
|
||||
<div class="hint">
|
||||
{{ $t('task.attachment.drop') }}
|
||||
</div>
|
||||
<div class="hint">{{ $t('task.attachment.drop') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
@ -142,7 +148,7 @@
|
|||
|
||||
<template #text>
|
||||
<p>
|
||||
{{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br/>
|
||||
{{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br>
|
||||
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
|
||||
</p>
|
||||
</template>
|
||||
|
@ -153,7 +159,10 @@
|
|||
:enabled="attachmentImageBlobUrl !== null"
|
||||
@close="attachmentImageBlobUrl = null"
|
||||
>
|
||||
<img :src="attachmentImageBlobUrl" alt=""/>
|
||||
<img
|
||||
:src="attachmentImageBlobUrl"
|
||||
alt=""
|
||||
>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -163,6 +172,7 @@ import {ref, shallowReactive, computed} from 'vue'
|
|||
import {useDropZone} from '@vueuse/core'
|
||||
|
||||
import User from '@/components/misc/user.vue'
|
||||
import ProgressBar from '@/components/misc/ProgressBar.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import AttachmentService from '@/services/attachment'
|
||||
|
@ -179,9 +189,6 @@ import {error, success} from '@/message'
|
|||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const {
|
||||
task,
|
||||
editEnabled = true,
|
||||
|
@ -189,9 +196,10 @@ const {
|
|||
task: ITask,
|
||||
editEnabled: boolean,
|
||||
}>()
|
||||
|
||||
// FIXME: this should go through the store
|
||||
const emit = defineEmits(['task-changed'])
|
||||
const emit = defineEmits(['taskChanged'])
|
||||
const taskStore = useTaskStore()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const attachmentService = shallowReactive(new AttachmentService())
|
||||
|
||||
|
@ -267,7 +275,7 @@ function copyUrl(attachment: IAttachment) {
|
|||
|
||||
async function setCoverImage(attachment: IAttachment | null) {
|
||||
const updatedTask = await taskStore.setCoverImage(task, attachment)
|
||||
emit('task-changed', updatedTask)
|
||||
emit('taskChanged', updatedTask)
|
||||
success({message: t('task.attachment.successfullyChangedCoverImage')})
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,9 +1,29 @@
|
|||
<template>
|
||||
<span v-if="checklist.total > 0" class="checklist-summary">
|
||||
<svg width="12" height="12">
|
||||
<circle stroke-width="2" fill="transparent" cx="50%" cy="50%" r="5"></circle>
|
||||
<circle stroke-width="2" stroke-dasharray="31" :stroke-dashoffset="checklistCircleDone"
|
||||
stroke-linecap="round" fill="transparent" cx="50%" cy="50%" r="5"></circle>
|
||||
<span
|
||||
v-if="checklist.total > 0"
|
||||
class="checklist-summary"
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
>
|
||||
<circle
|
||||
stroke-width="2"
|
||||
fill="transparent"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
r="5"
|
||||
/>
|
||||
<circle
|
||||
stroke-width="2"
|
||||
stroke-dasharray="31"
|
||||
:stroke-dashoffset="checklistCircleDone"
|
||||
stroke-linecap="round"
|
||||
fill="transparent"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
r="5"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ label }}</span>
|
||||
</span>
|
||||
|
|
|
@ -1,20 +1,30 @@
|
|||
<template>
|
||||
<div class="content details" v-if="enabled">
|
||||
<h3 v-if="canWrite || comments.length > 0" :class="{'d-print-none': comments.length === 0}">
|
||||
<div
|
||||
v-if="enabled"
|
||||
class="content details"
|
||||
>
|
||||
<h3
|
||||
v-if="canWrite || comments.length > 0"
|
||||
:class="{'d-print-none': comments.length === 0}"
|
||||
>
|
||||
<span class="icon is-grey">
|
||||
<icon :icon="['far', 'comments']"/>
|
||||
<icon :icon="['far', 'comments']" />
|
||||
</span>
|
||||
{{ $t('task.comment.title') }}
|
||||
</h3>
|
||||
<div class="comments">
|
||||
<span
|
||||
class="is-inline-flex is-align-items-center"
|
||||
v-if="taskCommentService.loading && saving === null && !creating"
|
||||
class="is-inline-flex is-align-items-center"
|
||||
>
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
<span class="loader is-inline-block mr-2" />
|
||||
{{ $t('task.comment.loading') }}
|
||||
</span>
|
||||
<div :key="c.id" class="media comment" v-for="c in comments">
|
||||
<div
|
||||
v-for="c in comments"
|
||||
:key="c.id"
|
||||
class="media comment"
|
||||
>
|
||||
<figure class="media-left is-hidden-mobile">
|
||||
<img
|
||||
:src="getAvatarUrl(c.author, 48)"
|
||||
|
@ -22,7 +32,7 @@
|
|||
class="image is-avatar"
|
||||
height="48"
|
||||
width="48"
|
||||
/>
|
||||
>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="comment-info">
|
||||
|
@ -32,9 +42,12 @@
|
|||
class="image is-avatar d-print-none"
|
||||
height="20"
|
||||
width="20"
|
||||
/>
|
||||
>
|
||||
<strong>{{ getDisplayName(c.author) }}</strong>
|
||||
<span v-tooltip="formatDateLong(c.created)" class="has-text-grey">
|
||||
<span
|
||||
v-tooltip="formatDateLong(c.created)"
|
||||
class="has-text-grey"
|
||||
>
|
||||
{{ formatDateSince(c.created) }}
|
||||
</span>
|
||||
<span
|
||||
|
@ -45,32 +58,35 @@
|
|||
</span>
|
||||
<CustomTransition name="fade">
|
||||
<span
|
||||
class="is-inline-flex"
|
||||
v-if="
|
||||
taskCommentService.loading &&
|
||||
saving === c.id
|
||||
"
|
||||
class="is-inline-flex"
|
||||
>
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
<span class="loader is-inline-block mr-2" />
|
||||
{{ $t('misc.saving') }}
|
||||
</span>
|
||||
<span
|
||||
class="has-text-success"
|
||||
v-else-if="
|
||||
!taskCommentService.loading &&
|
||||
saved === c.id
|
||||
"
|
||||
class="has-text-success"
|
||||
>
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
</CustomTransition>
|
||||
</div>
|
||||
<editor
|
||||
<Editor
|
||||
v-model="c.comment"
|
||||
:is-edit-enabled="canWrite && c.author.id === currentUserId"
|
||||
:upload-callback="attachmentUpload"
|
||||
:upload-enabled="true"
|
||||
v-model="c.comment"
|
||||
@update:model-value="
|
||||
:bottom-actions="actions[c.id]"
|
||||
:show-save="true"
|
||||
initial-mode="preview"
|
||||
@update:modelValue="
|
||||
() => {
|
||||
toggleEdit(c)
|
||||
editCommentWithDelay()
|
||||
|
@ -80,13 +96,13 @@
|
|||
toggleEdit(c)
|
||||
editComment()
|
||||
}"
|
||||
:bottom-actions="actions[c.id]"
|
||||
:show-save="true"
|
||||
initial-mode="preview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media comment d-print-none" v-if="canWrite">
|
||||
<div
|
||||
v-if="canWrite"
|
||||
class="media comment d-print-none"
|
||||
>
|
||||
<figure class="media-left is-hidden-mobile">
|
||||
<img
|
||||
:src="userAvatar"
|
||||
|
@ -94,7 +110,7 @@
|
|||
class="image is-avatar"
|
||||
height="48"
|
||||
width="48"
|
||||
/>
|
||||
>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="form">
|
||||
|
@ -103,12 +119,14 @@
|
|||
v-if="taskCommentService.loading && creating"
|
||||
class="is-inline-flex"
|
||||
>
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
<span class="loader is-inline-block mr-2" />
|
||||
{{ $t('task.comment.creating') }}
|
||||
</span>
|
||||
</CustomTransition>
|
||||
<div class="field">
|
||||
<editor
|
||||
<Editor
|
||||
v-if="editorActive"
|
||||
v-model="newComment.comment"
|
||||
:class="{
|
||||
'is-loading':
|
||||
taskCommentService.loading &&
|
||||
|
@ -117,8 +135,6 @@
|
|||
:upload-callback="attachmentUpload"
|
||||
:upload-enabled="true"
|
||||
:placeholder="$t('task.comment.placeholder')"
|
||||
v-if="editorActive"
|
||||
v-model="newComment.comment"
|
||||
@save="addComment()"
|
||||
/>
|
||||
</div>
|
||||
|
@ -141,11 +157,13 @@
|
|||
@close="showDeleteModal = false"
|
||||
@submit="() => deleteComment(commentToDelete)"
|
||||
>
|
||||
<template #header><span>{{ $t('task.comment.delete') }}</span></template>
|
||||
<template #header>
|
||||
<span>{{ $t('task.comment.delete') }}</span>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<p>
|
||||
{{ $t('task.comment.deleteText1') }}<br/>
|
||||
{{ $t('task.comment.deleteText1') }}<br>
|
||||
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
|
||||
</p>
|
||||
</template>
|
||||
|
|
|
@ -1,24 +1,42 @@
|
|||
<template>
|
||||
<p class="created">
|
||||
<time :datetime="formatISO(task.created)" v-tooltip="formatDateLong(task.created)">
|
||||
<i18n-t keypath="task.detail.created" scope="global">
|
||||
<time
|
||||
v-tooltip="formatDateLong(task.created)"
|
||||
:datetime="formatISO(task.created)"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="task.detail.created"
|
||||
scope="global"
|
||||
>
|
||||
<span>{{ formatDateSince(task.created) }}</span>
|
||||
{{ getDisplayName(task.createdBy) }}
|
||||
</i18n-t>
|
||||
</time>
|
||||
<template v-if="+new Date(task.created) !== +new Date(task.updated)">
|
||||
<br/>
|
||||
<br>
|
||||
<!-- Computed properties to show the actual date every time it gets updated -->
|
||||
<time :datetime="formatISO(task.updated)" v-tooltip="updatedFormatted">
|
||||
<i18n-t keypath="task.detail.updated" scope="global">
|
||||
<time
|
||||
v-tooltip="updatedFormatted"
|
||||
:datetime="formatISO(task.updated)"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="task.detail.updated"
|
||||
scope="global"
|
||||
>
|
||||
<span>{{ updatedSince }}</span>
|
||||
</i18n-t>
|
||||
</time>
|
||||
</template>
|
||||
<template v-if="task.done">
|
||||
<br/>
|
||||
<time :datetime="formatISO(task.doneAt)" v-tooltip="doneFormatted">
|
||||
<i18n-t keypath="task.detail.doneAt" scope="global">
|
||||
<br>
|
||||
<time
|
||||
v-tooltip="doneFormatted"
|
||||
:datetime="formatISO(task.doneAt)"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="task.detail.doneAt"
|
||||
scope="global"
|
||||
>
|
||||
<span>{{ doneSince }}</span>
|
||||
</i18n-t>
|
||||
</time>
|
||||
|
|
|
@ -6,33 +6,33 @@
|
|||
<label class="label">{{ $t('task.deferDueDate.title') }}</label>
|
||||
<div class="defer-days">
|
||||
<x-button
|
||||
@click.prevent.stop="() => deferDays(1)"
|
||||
:shadow="false"
|
||||
variant="secondary"
|
||||
@click.prevent.stop="() => deferDays(1)"
|
||||
>
|
||||
{{ $t('task.deferDueDate.1day') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click.prevent.stop="() => deferDays(3)"
|
||||
:shadow="false"
|
||||
variant="secondary"
|
||||
@click.prevent.stop="() => deferDays(3)"
|
||||
>
|
||||
{{ $t('task.deferDueDate.3days') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click.prevent.stop="() => deferDays(7)"
|
||||
:shadow="false"
|
||||
variant="secondary"
|
||||
@click.prevent.stop="() => deferDays(7)"
|
||||
>
|
||||
{{ $t('task.deferDueDate.1week') }}
|
||||
</x-button>
|
||||
</div>
|
||||
<flat-pickr
|
||||
v-model="dueDate"
|
||||
:class="{ disabled: taskService.loading }"
|
||||
:config="flatPickerConfig"
|
||||
:disabled="taskService.loading || undefined"
|
||||
class="input"
|
||||
v-model="dueDate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -44,7 +44,7 @@ import flatPickr from 'vue-flatpickr-component'
|
|||
|
||||
import TaskService from '@/services/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
|
@ -55,7 +55,6 @@ const {
|
|||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const task = ref<ITask>()
|
||||
|
@ -102,9 +101,7 @@ const flatPickerConfig = computed(() => ({
|
|||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
locale: {
|
||||
firstDayOfWeek: authStore.settings.weekStart,
|
||||
},
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
|
||||
function deferDays(days: number) {
|
||||
|
|
|
@ -2,29 +2,35 @@
|
|||
<div>
|
||||
<h3>
|
||||
<span class="icon is-grey">
|
||||
<icon icon="align-left"/>
|
||||
<icon icon="align-left" />
|
||||
</span>
|
||||
{{ $t('task.attributes.description') }}
|
||||
<CustomTransition name="fade">
|
||||
<span class="is-small is-inline-flex" v-if="loading && saving">
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
<span
|
||||
v-if="loading && saving"
|
||||
class="is-small is-inline-flex"
|
||||
>
|
||||
<span class="loader is-inline-block mr-2" />
|
||||
{{ $t('misc.saving') }}
|
||||
</span>
|
||||
<span class="is-small has-text-success" v-else-if="!loading && saved">
|
||||
<icon icon="check"/>
|
||||
<span
|
||||
v-else-if="!loading && saved"
|
||||
class="is-small has-text-success"
|
||||
>
|
||||
<icon icon="check" />
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
</CustomTransition>
|
||||
</h3>
|
||||
<editor
|
||||
<Editor
|
||||
v-model="description"
|
||||
class="tiptap__task-description"
|
||||
:is-edit-enabled="canWrite"
|
||||
:upload-callback="uploadCallback"
|
||||
:placeholder="$t('task.description.placeholder')"
|
||||
:show-save="true"
|
||||
edit-shortcut="e"
|
||||
v-model="description"
|
||||
@update:model-value="saveWithDelay"
|
||||
@update:modelValue="saveWithDelay"
|
||||
@save="save"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,31 @@
|
|||
<template>
|
||||
<Multiselect
|
||||
v-model="assignees"
|
||||
class="edit-assignees"
|
||||
:class="{'has-assignees': assignees.length > 0}"
|
||||
:loading="projectUserService.loading"
|
||||
:placeholder="$t('task.assignee.placeholder')"
|
||||
:multiple="true"
|
||||
@search="findUser"
|
||||
:search-results="foundUsers"
|
||||
@select="addAssignee"
|
||||
label="name"
|
||||
:select-placeholder="$t('task.assignee.selectPlaceholder')"
|
||||
v-model="assignees"
|
||||
:autocomplete-enabled="false"
|
||||
@search="findUser"
|
||||
@select="addAssignee"
|
||||
>
|
||||
<template #items="{items}">
|
||||
<assignee-list :assignees="items" :remove="removeAssignee"/>
|
||||
<AssigneeList
|
||||
:assignees="items"
|
||||
:remove="removeAssignee"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</template>
|
||||
<template #searchResult="{option: user}">
|
||||
<user :avatar-size="24" :show-username="true" :user="user"/>
|
||||
<User
|
||||
:avatar-size="24"
|
||||
:show-username="true"
|
||||
:user="user"
|
||||
/>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</template>
|
||||
|
@ -115,3 +125,9 @@ async function findUser(query: string) {
|
|||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.edit-assignees.has-assignees.multiselect .input {
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,37 +1,44 @@
|
|||
<template>
|
||||
<Multiselect
|
||||
v-model="labels"
|
||||
:loading="loading"
|
||||
:placeholder="$t('task.label.placeholder')"
|
||||
:multiple="true"
|
||||
@search="findLabel"
|
||||
:search-results="foundLabels"
|
||||
@select="addLabel"
|
||||
label="title"
|
||||
:creatable="creatable"
|
||||
@create="createAndAddLabel"
|
||||
:create-placeholder="$t('task.label.createPlaceholder')"
|
||||
v-model="labels"
|
||||
:search-delay="10"
|
||||
:close-after-select="false"
|
||||
@search="findLabel"
|
||||
@select="addLabel"
|
||||
@create="createAndAddLabel"
|
||||
>
|
||||
<template #tag="{item: label}">
|
||||
<span
|
||||
:style="{'background': label.hexColor, 'color': label.textColor}"
|
||||
class="tag">
|
||||
class="tag"
|
||||
>
|
||||
<span>{{ label.title }}</span>
|
||||
<BaseButton v-cy="'taskDetail.removeLabel'" @click="removeLabel(label)" class="delete is-small" />
|
||||
<BaseButton
|
||||
v-cy="'taskDetail.removeLabel'"
|
||||
class="delete is-small"
|
||||
@click="removeLabel(label)"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<template #searchResult="{option}">
|
||||
<span
|
||||
v-if="typeof option === 'string'"
|
||||
class="tag search-result">
|
||||
class="tag search-result"
|
||||
>
|
||||
<span>{{ option }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:style="{'background': option.hexColor, 'color': option.textColor}"
|
||||
class="tag search-result">
|
||||
class="tag search-result"
|
||||
>
|
||||
<span>{{ option.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
<template>
|
||||
<div class="heading">
|
||||
<div class="flex is-align-items-center">
|
||||
<BaseButton @click="copyUrl"><h1 class="title task-id">{{ textIdentifier }}</h1></BaseButton>
|
||||
<Done class="heading__done" :is-done="task.done"/>
|
||||
<BaseButton @click="copyUrl">
|
||||
<h1 class="title task-id">
|
||||
{{ textIdentifier }}
|
||||
</h1>
|
||||
</BaseButton>
|
||||
<Done
|
||||
class="heading__done"
|
||||
:is-done="task.done"
|
||||
/>
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
|
@ -12,10 +19,10 @@
|
|||
<h1
|
||||
class="title input"
|
||||
:class="{'disabled': !canWrite}"
|
||||
@blur="save(($event.target as HTMLInputElement).textContent as string)"
|
||||
@keydown.enter.prevent.stop="($event.target as HTMLInputElement).blur()"
|
||||
:contenteditable="canWrite ? true : undefined"
|
||||
:spellcheck="false"
|
||||
@blur="save(($event.target as HTMLInputElement).textContent as string)"
|
||||
@keydown.enter.prevent.stop="($event.target as HTMLInputElement).blur()"
|
||||
>
|
||||
{{ task.title.trim() }}
|
||||
</h1>
|
||||
|
@ -24,14 +31,17 @@
|
|||
v-if="loading && saving"
|
||||
class="is-inline-flex is-align-items-center"
|
||||
>
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
<span class="loader is-inline-block mr-2" />
|
||||
{{ $t('misc.saving') }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="!loading && showSavedMessage"
|
||||
class="has-text-success is-inline-flex is-align-content-center"
|
||||
>
|
||||
<icon icon="check" class="mr-2"/>
|
||||
<icon
|
||||
icon="check"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
</CustomTransition>
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
:class="{
|
||||
'is-loading': loadingInternal || loading,
|
||||
'draggable': !(loadingInternal || loading),
|
||||
'has-light-text': color !== TASK_DEFAULT_COLOR && !colorIsDark(color),
|
||||
'has-custom-background-color': color !== TASK_DEFAULT_COLOR ? color : undefined,
|
||||
'has-light-text': !colorIsDark(color),
|
||||
'has-custom-background-color': color ?? undefined,
|
||||
}"
|
||||
:style="{'background-color': color !== TASK_DEFAULT_COLOR ? color : undefined}"
|
||||
:style="{'background-color': color ?? undefined}"
|
||||
@click.exact="openTaskDetail()"
|
||||
@click.ctrl="() => toggleTaskDone(task)"
|
||||
@click.meta="() => toggleTaskDone(task)"
|
||||
|
@ -17,10 +17,14 @@
|
|||
:src="coverImageBlobUrl"
|
||||
alt=""
|
||||
class="cover-image"
|
||||
/>
|
||||
>
|
||||
<div class="p-2">
|
||||
<span class="task-id">
|
||||
<Done class="kanban-card__done" :is-done="task.done" variant="small"/>
|
||||
<Done
|
||||
class="kanban-card__done"
|
||||
:is-done="task.done"
|
||||
variant="small"
|
||||
/>
|
||||
<template v-if="task.identifier === ''">
|
||||
#{{ task.index }}
|
||||
</template>
|
||||
|
@ -29,46 +33,59 @@
|
|||
</template>
|
||||
</span>
|
||||
<span
|
||||
v-if="task.dueDate > 0"
|
||||
v-tooltip="formatDateLong(task.dueDate)"
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
class="due-date"
|
||||
v-if="task.dueDate > 0"
|
||||
v-tooltip="formatDateLong(task.dueDate)">
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
<icon :icon="['far', 'calendar-alt']" />
|
||||
</span>
|
||||
<time :datetime="formatISO(task.dueDate)">
|
||||
{{ formatDateSince(task.dueDate) }}
|
||||
</time>
|
||||
</span>
|
||||
<h3>{{ task.title }}</h3>
|
||||
<progress
|
||||
class="progress is-small"
|
||||
|
||||
<ProgressBar
|
||||
v-if="task.percentDone > 0"
|
||||
:value="task.percentDone * 100" max="100">
|
||||
{{ task.percentDone * 100 }}%
|
||||
</progress>
|
||||
class="task-progress"
|
||||
:value="task.percentDone * 100"
|
||||
/>
|
||||
<div class="footer">
|
||||
<labels :labels="task.labels"/>
|
||||
<priority-label
|
||||
<Labels :labels="task.labels" />
|
||||
<PriorityLabel
|
||||
:priority="task.priority"
|
||||
:done="task.done"
|
||||
class="is-inline-flex is-align-items-center"/>
|
||||
<assignee-list
|
||||
class="is-inline-flex is-align-items-center"
|
||||
/>
|
||||
<AssigneeList
|
||||
v-if="task.assignees.length > 0"
|
||||
:assignees="task.assignees"
|
||||
:avatar-size="24"
|
||||
class="ml-1"
|
||||
:inline="true"
|
||||
class="mr-1"
|
||||
/>
|
||||
<checklist-summary :task="task"/>
|
||||
<span class="icon" v-if="task.attachments.length > 0">
|
||||
<icon icon="paperclip"/>
|
||||
<ChecklistSummary
|
||||
:task="task"
|
||||
class="checklist"
|
||||
/>
|
||||
<span
|
||||
v-if="task.attachments.length > 0"
|
||||
class="icon"
|
||||
>
|
||||
<icon icon="paperclip" />
|
||||
</span>
|
||||
<span v-if="!isEditorContentEmpty(task.description)" class="icon">
|
||||
<icon icon="align-left"/>
|
||||
<span
|
||||
v-if="!isEditorContentEmpty(task.description)"
|
||||
class="icon"
|
||||
>
|
||||
<icon icon="align-left" />
|
||||
</span>
|
||||
<span class="icon" v-if="task.repeatAfter.amount > 0">
|
||||
<icon icon="history"/>
|
||||
<span
|
||||
v-if="task.repeatAfter.amount > 0"
|
||||
class="icon"
|
||||
>
|
||||
<icon icon="history" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -80,11 +97,12 @@ import {ref, computed, watch} from 'vue'
|
|||
import {useRouter} from 'vue-router'
|
||||
|
||||
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
||||
import ProgressBar from '@/components/misc/ProgressBar.vue'
|
||||
import Done from '@/components/misc/Done.vue'
|
||||
import Labels from '@/components/tasks/partials/labels.vue'
|
||||
import ChecklistSummary from './checklist-summary.vue'
|
||||
|
||||
import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task'
|
||||
import {getHexColor} from '@/models/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
|
||||
import AttachmentService from '@/services/attachment'
|
||||
|
@ -97,10 +115,6 @@ import {useAuthStore} from '@/stores/auth'
|
|||
import {playPopSound} from '@/helpers/playPop'
|
||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loadingInternal = ref(false)
|
||||
|
||||
const {
|
||||
task,
|
||||
loading = false,
|
||||
|
@ -109,6 +123,10 @@ const {
|
|||
loading: boolean,
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loadingInternal = ref(false)
|
||||
|
||||
const color = computed(() => getHexColor(task.hexColor))
|
||||
|
||||
async function toggleTaskDone(task: ITask) {
|
||||
|
@ -188,11 +206,6 @@ $task-background: var(--white);
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin: 8px 0 0 0;
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
.due-date {
|
||||
float: right;
|
||||
|
@ -218,15 +231,20 @@ $task-background: var(--white);
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: .25rem;
|
||||
|
||||
:deep(.tag),
|
||||
:deep(.checklist-summary),
|
||||
.assignees,
|
||||
.icon,
|
||||
.priority-label {
|
||||
margin-top: .25rem;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
:deep(.checklist-summary) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.assignees {
|
||||
display: flex;
|
||||
|
||||
|
@ -292,25 +310,34 @@ $task-background: var(--white);
|
|||
.priority-label {
|
||||
background: hsl(220, 13%, 91%);
|
||||
}
|
||||
|
||||
.footer :deep(.checklist-summary) {
|
||||
color: hsl(216.9, 19.1%, 26.7%); // grey-700
|
||||
}
|
||||
}
|
||||
|
||||
&.has-light-text {
|
||||
--white: hsla(var(--white-h), var(--white-s), var(--white-l), var(--white-a)) !important;
|
||||
color: var(--white);
|
||||
|
||||
.task-id {
|
||||
color: var(--grey-200);
|
||||
color: hsl(220, 13%, 91%); // grey-200;
|
||||
}
|
||||
|
||||
.footer .icon,
|
||||
.due-date,
|
||||
.priority-label {
|
||||
background: var(--grey-800);
|
||||
background: hsl(215, 27.9%, 16.9%); // grey-800
|
||||
}
|
||||
|
||||
.footer {
|
||||
.icon svg {
|
||||
fill: var(--white);
|
||||
}
|
||||
|
||||
:deep(.checklist-summary) {
|
||||
color: hsl(220, 13%, 91%); // grey-200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -318,4 +345,10 @@ $task-background: var(--white);
|
|||
.kanban-card__done {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
.task-progress {
|
||||
margin: 8px 0 0 0;
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
}
|
||||
</style>
|
|
@ -2,8 +2,8 @@
|
|||
<div class="label-wrapper">
|
||||
<XLabel
|
||||
v-for="label in labels"
|
||||
:label="label"
|
||||
:key="label.id"
|
||||
:label="label"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -25,5 +25,9 @@ defineProps({
|
|||
<style lang="scss" scoped>
|
||||
.label-wrapper {
|
||||
display: inline;
|
||||
|
||||
:deep(.tag) {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,17 +4,39 @@
|
|||
v-model.number="percentDone"
|
||||
:disabled="disabled || undefined"
|
||||
>
|
||||
<option value="0">0%</option>
|
||||
<option value="0.1">10%</option>
|
||||
<option value="0.2">20%</option>
|
||||
<option value="0.3">30%</option>
|
||||
<option value="0.4">40%</option>
|
||||
<option value="0.5">50%</option>
|
||||
<option value="0.6">60%</option>
|
||||
<option value="0.7">70%</option>
|
||||
<option value="0.8">80%</option>
|
||||
<option value="0.9">90%</option>
|
||||
<option value="1">100%</option>
|
||||
<option value="0">
|
||||
0%
|
||||
</option>
|
||||
<option value="0.1">
|
||||
10%
|
||||
</option>
|
||||
<option value="0.2">
|
||||
20%
|
||||
</option>
|
||||
<option value="0.3">
|
||||
30%
|
||||
</option>
|
||||
<option value="0.4">
|
||||
40%
|
||||
</option>
|
||||
<option value="0.5">
|
||||
50%
|
||||
</option>
|
||||
<option value="0.6">
|
||||
60%
|
||||
</option>
|
||||
<option value="0.7">
|
||||
70%
|
||||
</option>
|
||||
<option value="0.8">
|
||||
80%
|
||||
</option>
|
||||
<option value="0.9">
|
||||
90%
|
||||
</option>
|
||||
<option value="1">
|
||||
100%
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
<template>
|
||||
<span
|
||||
v-if="!done && (showAll || priority >= priorities.HIGH)"
|
||||
:class="{'not-so-high': priority === priorities.HIGH, 'high-priority': priority >= priorities.HIGH}"
|
||||
class="priority-label"
|
||||
v-if="!done && (showAll || priority >= priorities.HIGH)">
|
||||
<span class="icon" v-if="priority >= priorities.HIGH">
|
||||
<icon icon="exclamation"/>
|
||||
>
|
||||
<span
|
||||
v-if="priority >= priorities.HIGH"
|
||||
class="icon"
|
||||
>
|
||||
<icon icon="exclamation" />
|
||||
</span>
|
||||
<span>
|
||||
<template v-if="priority === priorities.UNSET">{{ $t('task.priority.unset') }}</template>
|
||||
|
@ -14,8 +18,11 @@
|
|||
<template v-if="priority === priorities.URGENT">{{ $t('task.priority.urgent') }}</template>
|
||||
<template v-if="priority === priorities.DO_NOW">{{ $t('task.priority.doNow') }}</template>
|
||||
</span>
|
||||
<span class="icon pr-0" v-if="priority === priorities.DO_NOW">
|
||||
<icon icon="exclamation"/>
|
||||
<span
|
||||
v-if="priority === priorities.DO_NOW"
|
||||
class="icon pr-0"
|
||||
>
|
||||
<icon icon="exclamation" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -2,15 +2,27 @@
|
|||
<div class="select">
|
||||
<select
|
||||
v-model="priority"
|
||||
@change="updateData"
|
||||
:disabled="disabled || undefined"
|
||||
@change="updateData"
|
||||
>
|
||||
<option :value="PRIORITIES.UNSET">{{ $t('task.priority.unset') }}</option>
|
||||
<option :value="PRIORITIES.LOW">{{ $t('task.priority.low') }}</option>
|
||||
<option :value="PRIORITIES.MEDIUM">{{ $t('task.priority.medium') }}</option>
|
||||
<option :value="PRIORITIES.HIGH">{{ $t('task.priority.high') }}</option>
|
||||
<option :value="PRIORITIES.URGENT">{{ $t('task.priority.urgent') }}</option>
|
||||
<option :value="PRIORITIES.DO_NOW">{{ $t('task.priority.doNow') }}</option>
|
||||
<option :value="PRIORITIES.UNSET">
|
||||
{{ $t('task.priority.unset') }}
|
||||
</option>
|
||||
<option :value="PRIORITIES.LOW">
|
||||
{{ $t('task.priority.low') }}
|
||||
</option>
|
||||
<option :value="PRIORITIES.MEDIUM">
|
||||
{{ $t('task.priority.medium') }}
|
||||
</option>
|
||||
<option :value="PRIORITIES.HIGH">
|
||||
{{ $t('task.priority.high') }}
|
||||
</option>
|
||||
<option :value="PRIORITIES.URGENT">
|
||||
{{ $t('task.priority.urgent') }}
|
||||
</option>
|
||||
<option :value="PRIORITIES.DO_NOW">
|
||||
{{ $t('task.priority.doNow') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
label="title"
|
||||
:select-placeholder="$t('project.searchSelect')"
|
||||
:model-value="project"
|
||||
@update:model-value="Object.assign(project, $event)"
|
||||
@update:modelValue="Object.assign(project, $event)"
|
||||
@select="select"
|
||||
@search="findProjects"
|
||||
>
|
||||
<template #searchResult="{option}">
|
||||
<span class="has-text-grey" v-if="projectStore.getAncestors(option).length > 1">
|
||||
<span
|
||||
v-if="projectStore.getAncestors(option).length > 1"
|
||||
class="has-text-grey"
|
||||
>
|
||||
{{ projectStore.getAncestors(option).filter(p => p.id !== option.id).map(p => getProjectTitle(p)).join(' > ') }} >
|
||||
</span>
|
||||
{{ getProjectTitle(option) }}
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
<template>
|
||||
<template v-if="mode !== 'disabled' && prefixes !== undefined">
|
||||
<BaseButton
|
||||
@click="() => visible = true"
|
||||
class="icon is-small show-helper-text"
|
||||
v-tooltip="$t('task.quickAddMagic.hint')"
|
||||
class="icon is-small show-helper-text"
|
||||
:aria-label="$t('task.quickAddMagic.hint')"
|
||||
:class="{'is-highlighted': highlightHintIcon}"
|
||||
@click="() => visible = true"
|
||||
>
|
||||
<icon :icon="['far', 'circle-question']"/>
|
||||
<icon :icon="['far', 'circle-question']" />
|
||||
</BaseButton>
|
||||
<modal
|
||||
:enabled="visible"
|
||||
@close="() => visible = false"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
@close="() => visible = false"
|
||||
>
|
||||
<card
|
||||
class="has-no-shadow"
|
||||
:title="$t('task.quickAddMagic.title')"
|
||||
>
|
||||
<card class="has-no-shadow" :title="$t('task.quickAddMagic.title')">
|
||||
<p>{{ $t('task.quickAddMagic.intro') }}</p>
|
||||
|
||||
<h3>{{ $t('task.attributes.labels') }}</h3>
|
||||
|
@ -102,15 +105,15 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
import {PREFIXES} from '@/modules/parseTaskText'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
defineProps<{
|
||||
highlightHintIcon?: boolean,
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const visible = ref(false)
|
||||
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
|
||||
|
||||
defineProps<{
|
||||
highlightHintIcon?: boolean,
|
||||
}>()
|
||||
|
||||
const prefixes = computed(() => PREFIXES[mode.value])
|
||||
</script>
|
||||
|
||||
|
|
|
@ -2,41 +2,53 @@
|
|||
<div class="task-relations">
|
||||
<x-button
|
||||
v-if="editEnabled && Object.keys(relatedTasks).length > 0"
|
||||
@click="showNewRelationForm = !showNewRelationForm"
|
||||
id="showRelatedTasksFormButton"
|
||||
v-tooltip="$t('task.relation.add')"
|
||||
class="is-pulled-right add-task-relation-button d-print-none"
|
||||
:class="{'is-active': showNewRelationForm}"
|
||||
v-tooltip="$t('task.relation.add')"
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
:shadow="false"
|
||||
id="showRelatedTasksFormButton"
|
||||
@click="showNewRelationForm = !showNewRelationForm"
|
||||
/>
|
||||
<transition-group name="fade">
|
||||
<template v-if="editEnabled && showCreate">
|
||||
<label class="label" key="label">
|
||||
<label
|
||||
key="label"
|
||||
class="label"
|
||||
>
|
||||
{{ $t('task.relation.new') }}
|
||||
<CustomTransition name="fade">
|
||||
<span class="is-inline-flex" v-if="taskRelationService.loading">
|
||||
<span class="loader is-inline-block mr-2"></span>
|
||||
<span
|
||||
v-if="taskRelationService.loading"
|
||||
class="is-inline-flex"
|
||||
>
|
||||
<span class="loader is-inline-block mr-2" />
|
||||
{{ $t('misc.saving') }}
|
||||
</span>
|
||||
<span class="has-text-success" v-else-if="!taskRelationService.loading && saved">
|
||||
<span
|
||||
v-else-if="!taskRelationService.loading && saved"
|
||||
class="has-text-success"
|
||||
>
|
||||
{{ $t('misc.saved') }}
|
||||
</span>
|
||||
</CustomTransition>
|
||||
</label>
|
||||
<div class="field" key="field-search">
|
||||
<div
|
||||
key="field-search"
|
||||
class="field"
|
||||
>
|
||||
<Multiselect
|
||||
v-model="newTaskRelation.task"
|
||||
v-focus
|
||||
:placeholder="$t('task.relation.searchPlaceholder')"
|
||||
@search="findTasks"
|
||||
:loading="taskService.loading"
|
||||
:search-results="mappedFoundTasks"
|
||||
label="title"
|
||||
v-model="newTaskRelation.task"
|
||||
:creatable="true"
|
||||
:create-placeholder="$t('task.relation.createPlaceholder')"
|
||||
@search="findTasks"
|
||||
@create="createAndRelateTask"
|
||||
v-focus
|
||||
>
|
||||
<template #searchResult="{option: task}">
|
||||
<span
|
||||
|
@ -45,62 +57,86 @@
|
|||
:class="{'is-strikethrough': task.done}"
|
||||
>
|
||||
<span
|
||||
class="different-project"
|
||||
v-if="task.projectId !== projectId"
|
||||
class="different-project"
|
||||
>
|
||||
<span
|
||||
v-if="task.differentProject !== null"
|
||||
v-tooltip="$t('task.relation.differentProject')">
|
||||
v-tooltip="$t('task.relation.differentProject')"
|
||||
>
|
||||
{{ task.differentProject }} >
|
||||
</span>
|
||||
</span>
|
||||
{{ task.title }}
|
||||
</span>
|
||||
<span class="search-result" v-else>
|
||||
<span
|
||||
v-else
|
||||
class="search-result"
|
||||
>
|
||||
{{ task }}
|
||||
</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<div class="field has-addons mb-4" key="field-kind">
|
||||
<div
|
||||
key="field-kind"
|
||||
class="field has-addons mb-4"
|
||||
>
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth has-defaults">
|
||||
<select v-model="newTaskRelation.kind">
|
||||
<option value="unset">{{ $t('task.relation.select') }}</option>
|
||||
<option :key="`option_${rk}`" :value="rk" v-for="rk in RELATION_KINDS">
|
||||
<option value="unset">
|
||||
{{ $t('task.relation.select') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="rk in RELATION_KINDS"
|
||||
:key="`option_${rk}`"
|
||||
:value="rk"
|
||||
>
|
||||
{{ $t(`task.relation.kinds.${rk}`, 1) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button @click="addTaskRelation()">{{ $t('task.relation.add') }}</x-button>
|
||||
<x-button @click="addTaskRelation()">
|
||||
{{ $t('task.relation.add') }}
|
||||
</x-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</transition-group>
|
||||
|
||||
<div :key="rts.kind" class="related-tasks" v-for="rts in mappedRelatedTasks">
|
||||
<div
|
||||
v-for="rts in mappedRelatedTasks"
|
||||
:key="rts.kind"
|
||||
class="related-tasks"
|
||||
>
|
||||
<span class="title">{{ rts.title }}</span>
|
||||
<div class="tasks">
|
||||
<div :key="t.id" class="task" v-for="t in rts.tasks">
|
||||
<div
|
||||
v-for="t in rts.tasks"
|
||||
:key="t.id"
|
||||
class="task"
|
||||
>
|
||||
<div class="is-flex is-align-items-center">
|
||||
<Fancycheckbox
|
||||
class="task-done-checkbox"
|
||||
v-model="t.done"
|
||||
@update:model-value="toggleTaskDone(t)"
|
||||
class="task-done-checkbox"
|
||||
@update:modelValue="toggleTaskDone(t)"
|
||||
/>
|
||||
<router-link
|
||||
:to="{ name: route.name as string, params: { id: t.id } }"
|
||||
:class="{ 'is-strikethrough': t.done}"
|
||||
>
|
||||
<span
|
||||
class="different-project"
|
||||
v-if="t.projectId !== projectId"
|
||||
class="different-project"
|
||||
>
|
||||
<span
|
||||
v-if="t.differentProject !== null"
|
||||
v-tooltip="$t('task.relation.differentProject')">
|
||||
v-tooltip="$t('task.relation.differentProject')"
|
||||
>
|
||||
{{ t.differentProject }} >
|
||||
</span>
|
||||
</span>
|
||||
|
@ -109,18 +145,21 @@
|
|||
</div>
|
||||
<BaseButton
|
||||
v-if="editEnabled"
|
||||
class="remove"
|
||||
@click="setRelationToDelete({
|
||||
relationKind: rts.kind,
|
||||
otherTaskId: t.id
|
||||
})"
|
||||
class="remove"
|
||||
>
|
||||
<icon icon="trash-alt"/>
|
||||
<icon icon="trash-alt" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="none" v-if="showNoRelationsNotice && Object.keys(relatedTasks).length === 0">
|
||||
<p
|
||||
v-if="showNoRelationsNotice && Object.keys(relatedTasks).length === 0"
|
||||
class="none"
|
||||
>
|
||||
{{ $t('task.relation.noneYet') }}
|
||||
</p>
|
||||
|
||||
|
@ -129,11 +168,13 @@
|
|||
@close="relationToDelete = undefined"
|
||||
@submit="removeTaskRelation()"
|
||||
>
|
||||
<template #header><span>{{ $t('task.relation.delete') }}</span></template>
|
||||
<template #header>
|
||||
<span>{{ $t('task.relation.delete') }}</span>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<p>
|
||||
{{ $t('task.relation.deleteText1') }}<br/>
|
||||
{{ $t('task.relation.deleteText1') }}<br>
|
||||
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
|
||||
</p>
|
||||
</template>
|
||||
|
|
|
@ -10,8 +10,15 @@
|
|||
</SimpleButton>
|
||||
</template>
|
||||
<template #content="{isOpen, close}">
|
||||
<Card class="reminder-options-popup" :class="{'is-open': isOpen}" :padding="false">
|
||||
<div class="options" v-if="activeForm === null">
|
||||
<Card
|
||||
class="reminder-options-popup"
|
||||
:class="{'is-open': isOpen}"
|
||||
:padding="false"
|
||||
>
|
||||
<div
|
||||
v-if="activeForm === null"
|
||||
class="options"
|
||||
>
|
||||
<SimpleButton
|
||||
v-for="(p, k) in presets"
|
||||
:key="k"
|
||||
|
@ -22,16 +29,16 @@
|
|||
{{ formatReminder(p) }}
|
||||
</SimpleButton>
|
||||
<SimpleButton
|
||||
@click="showFormSwitch = 'relative'"
|
||||
class="option-button"
|
||||
:class="{'currently-active': typeof modelValue !== 'undefined' && modelValue?.relativeTo !== null && presets.find(p => p.relativePeriod === modelValue?.relativePeriod && modelValue?.relativeTo === p.relativeTo) === undefined}"
|
||||
@click="showFormSwitch = 'relative'"
|
||||
>
|
||||
{{ $t('task.reminder.custom') }}
|
||||
</SimpleButton>
|
||||
<SimpleButton
|
||||
@click="showFormSwitch = 'absolute'"
|
||||
class="option-button"
|
||||
:class="{'currently-active': modelValue?.relativeTo === null}"
|
||||
@click="showFormSwitch = 'absolute'"
|
||||
>
|
||||
{{ $t('task.reminder.dateAndTime') }}
|
||||
</SimpleButton>
|
||||
|
@ -46,7 +53,7 @@
|
|||
<DatepickerInline
|
||||
v-if="activeForm === 'absolute'"
|
||||
v-model="reminderDate"
|
||||
@update:modelValue="setReminderDate(close)"
|
||||
@update:modelValue="setReminderDateAndClose(close)"
|
||||
/>
|
||||
|
||||
<x-button
|
||||
|
@ -81,8 +88,6 @@ import TaskReminderModel from '@/models/taskReminder'
|
|||
import Card from '@/components/misc/card.vue'
|
||||
import SimpleButton from '@/components/input/SimpleButton.vue'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
clearAfterUpdate = false,
|
||||
|
@ -95,6 +100,8 @@ const {
|
|||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const reminder = ref<ITaskReminder>(new TaskReminderModel())
|
||||
|
||||
const presets = computed<TaskReminderModel[]>(() => [
|
||||
|
@ -105,7 +112,7 @@ const presets = computed<TaskReminderModel[]>(() => [
|
|||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 7, relativeTo: defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 30, relativeTo: defaultRelativeTo},
|
||||
])
|
||||
const reminderDate = ref(null)
|
||||
const reminderDate = ref<Date|null>(null)
|
||||
|
||||
type availableForms = null | 'relative' | 'absolute'
|
||||
|
||||
|
@ -135,7 +142,17 @@ const reminderText = computed(() => {
|
|||
watch(
|
||||
() => modelValue,
|
||||
(newReminder) => {
|
||||
reminder.value = newReminder || new TaskReminderModel()
|
||||
if(newReminder) {
|
||||
reminder.value = newReminder
|
||||
|
||||
if(newReminder.relativeTo === null) {
|
||||
reminderDate.value = new Date(newReminder.reminder)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
reminder.value = new TaskReminderModel()
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
@ -148,7 +165,7 @@ function updateData() {
|
|||
}
|
||||
}
|
||||
|
||||
function setReminderDate(close) {
|
||||
function setReminderDateAndClose(close) {
|
||||
reminder.value.reminder = reminderDate.value === null
|
||||
? null
|
||||
: new Date(reminderDate.value)
|
||||
|
|
|
@ -3,24 +3,38 @@
|
|||
class="reminder-period control"
|
||||
>
|
||||
<input
|
||||
class="input"
|
||||
v-model.number="period.duration"
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
@change="updateData"
|
||||
/>
|
||||
>
|
||||
|
||||
<div class="select">
|
||||
<select v-model="period.durationUnit" @change="updateData">
|
||||
<option value="minutes">{{ $t('time.units.minutes', period.duration) }}</option>
|
||||
<option value="hours">{{ $t('time.units.hours', period.duration) }}</option>
|
||||
<option value="days">{{ $t('time.units.days', period.duration) }}</option>
|
||||
<option value="weeks">{{ $t('time.units.weeks', period.duration) }}</option>
|
||||
<select
|
||||
v-model="period.durationUnit"
|
||||
@change="updateData"
|
||||
>
|
||||
<option value="minutes">
|
||||
{{ $t('time.units.minutes', period.duration) }}
|
||||
</option>
|
||||
<option value="hours">
|
||||
{{ $t('time.units.hours', period.duration) }}
|
||||
</option>
|
||||
<option value="days">
|
||||
{{ $t('time.units.days', period.duration) }}
|
||||
</option>
|
||||
<option value="weeks">
|
||||
{{ $t('time.units.weeks', period.duration) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="select">
|
||||
<select v-model.number="period.sign" @change="updateData">
|
||||
<select
|
||||
v-model.number="period.sign"
|
||||
@change="updateData"
|
||||
>
|
||||
<option value="-1">
|
||||
{{ $t('task.reminder.beforeShort') }}
|
||||
</option>
|
||||
|
@ -31,7 +45,10 @@
|
|||
</div>
|
||||
|
||||
<div class="select">
|
||||
<select v-model="period.relativeTo" @change="updateData">
|
||||
<select
|
||||
v-model="period.relativeTo"
|
||||
@change="updateData"
|
||||
>
|
||||
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE">
|
||||
{{ $t('task.attributes.dueDate') }}
|
||||
</option>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user