Merge branch 'main' into bulma-css-variables
# Conflicts: # src/App.vue # src/components/home/contentNoAuth.vue # src/components/home/topNavigation.vue # src/components/misc/api-config.vue # src/components/tasks/partials/relatedTasks.vue
This commit is contained in:
commit
2614fddf51
|
@ -151,6 +151,10 @@ steps:
|
||||||
- node ./scripts/deploy-preview-netlify.js
|
- node ./scripts/deploy-preview-netlify.js
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-prod
|
- build-prod
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
include:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
|
@ -651,6 +655,6 @@ steps:
|
||||||
from_secret: crowdin_key
|
from_secret: crowdin_key
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: b343ca4448b29bcd26ba9888fa67926040b2f420d891b62e2e74ce487557c58c
|
hmac: 15df446c7e93a881249d46273485183386157229ee6a37b1ed0fcb2a0b32bbe2
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
|
@ -24,12 +24,6 @@ RUN \
|
||||||
# Stage 2: copy
|
# Stage 2: copy
|
||||||
FROM nginx
|
FROM nginx
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y apt-utils openssl && \
|
|
||||||
mkdir -p /etc/nginx/ssl && \
|
|
||||||
openssl genrsa -out /etc/nginx/ssl/dummy.key 2048 && \
|
|
||||||
openssl req -new -key /etc/nginx/ssl/dummy.key -out /etc/nginx/ssl/dummy.csr -subj "/C=DE/L=Berlin/O=Vikunja/CN=Vikunja Snakeoil" && \
|
|
||||||
openssl x509 -req -days 3650 -in /etc/nginx/ssl/dummy.csr -signkey /etc/nginx/ssl/dummy.key -out /etc/nginx/ssl/dummy.crt
|
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY run.sh /run.sh
|
COPY run.sh /run.sh
|
||||||
|
|
||||||
|
|
|
@ -219,10 +219,10 @@ describe('Lists', () => {
|
||||||
cy.get('.table-view .filter-container .items .button')
|
cy.get('.table-view .filter-container .items .button')
|
||||||
.contains('Columns')
|
.contains('Columns')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.table-view .filter-container .card .card-content .fancycheckbox .check')
|
cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||||
.contains('Priority')
|
.contains('Priority')
|
||||||
.click()
|
.click()
|
||||||
cy.get('.table-view .filter-container .card .card-content .fancycheckbox .check')
|
cy.get('.table-view .filter-container .card.columns-filter .card-content .fancycheckbox .check')
|
||||||
.contains('Done')
|
.contains('Done')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
|
|
12
netlify.toml
12
netlify.toml
|
@ -1,3 +1,15 @@
|
||||||
[build]
|
[build]
|
||||||
command = "yarn build"
|
command = "yarn build"
|
||||||
publish = "dist"
|
publish = "dist"
|
||||||
|
|
||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "/index.html"
|
||||||
|
status = 200
|
||||||
|
|
||||||
|
[[headers]]
|
||||||
|
for = "/*"
|
||||||
|
[headers.values]
|
||||||
|
X-Frame-Options = "DENY"
|
||||||
|
X-XSS-Protection = "1; mode=block"
|
||||||
|
X-Robots-Tag = "noindex"
|
||||||
|
|
|
@ -60,15 +60,11 @@ http {
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
||||||
listen 443 default_server ssl http2;
|
|
||||||
|
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
expires $expires;
|
expires $expires;
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/ssl/dummy.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/ssl/dummy.key;
|
|
||||||
|
|
||||||
location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ {
|
location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"browserslist:update": "npx browserslist@latest --update-db"
|
"browserslist:update": "npx browserslist@latest --update-db"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@github/hotkey": "^1.6.0",
|
||||||
"@kyvg/vue3-notification": "2.3.4",
|
"@kyvg/vue3-notification": "2.3.4",
|
||||||
"@sentry/tracing": "6.14.3",
|
"@sentry/tracing": "6.14.3",
|
||||||
"@sentry/vue": "6.14.3",
|
"@sentry/vue": "6.14.3",
|
||||||
|
@ -82,7 +83,7 @@
|
||||||
"vite": "2.6.14",
|
"vite": "2.6.14",
|
||||||
"vite-plugin-pwa": "0.11.3",
|
"vite-plugin-pwa": "0.11.3",
|
||||||
"vue-tsc": "0.29.4",
|
"vue-tsc": "0.29.4",
|
||||||
"vite-svg-loader": "^3.1.0",
|
"vite-svg-loader": "3.1.0",
|
||||||
"wait-on": "6.0.0",
|
"wait-on": "6.0.0",
|
||||||
"workbox-cli": "6.3.0"
|
"workbox-cli": "6.3.0"
|
||||||
},
|
},
|
||||||
|
|
69
src/App.vue
69
src/App.vue
|
@ -1,25 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{'is-touch': isTouch}">
|
<ready>
|
||||||
<div :class="{'is-hidden': !online}">
|
<div :class="{'is-touch': isTouch}">
|
||||||
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
|
<div :class="{'is-hidden': !online}">
|
||||||
<div class="offline" style="height: 0;width: 0;"></div>
|
<template v-if="authUser">
|
||||||
<top-navigation v-if="authUser"/>
|
<top-navigation/>
|
||||||
<content-auth v-if="authUser"/>
|
<content-auth/>
|
||||||
<content-link-share v-else-if="authLinkShare"/>
|
</template>
|
||||||
<content-no-auth v-else/>
|
<content-link-share v-else-if="authLinkShare"/>
|
||||||
<notification/>
|
<content-no-auth v-else/>
|
||||||
</div>
|
<notification/>
|
||||||
<div class="app offline" v-if="!online">
|
|
||||||
<div class="offline-message">
|
|
||||||
<h1>You are offline.</h1>
|
|
||||||
<p>Please check your network connection and try again.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
</ready>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -36,6 +32,7 @@ import ContentLinkShare from './components/home/contentLinkShare'
|
||||||
import ContentNoAuth from './components/home/contentNoAuth'
|
import ContentNoAuth from './components/home/contentNoAuth'
|
||||||
import {setLanguage} from './i18n'
|
import {setLanguage} from './i18n'
|
||||||
import AccountDeleteService from '@/services/accountDelete'
|
import AccountDeleteService from '@/services/accountDelete'
|
||||||
|
import Ready from '@/components/misc/ready'
|
||||||
import {useColorScheme} from '@/composables/useColorScheme'
|
import {useColorScheme} from '@/composables/useColorScheme'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -47,6 +44,7 @@ export default defineComponent({
|
||||||
TopNavigation,
|
TopNavigation,
|
||||||
KeyboardShortcuts,
|
KeyboardShortcuts,
|
||||||
Notification,
|
Notification,
|
||||||
|
Ready,
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.setupOnlineStatus()
|
this.setupOnlineStatus()
|
||||||
|
@ -55,13 +53,6 @@ export default defineComponent({
|
||||||
this.setupAccountDeletionVerification()
|
this.setupAccountDeletionVerification()
|
||||||
},
|
},
|
||||||
beforeCreate() {
|
beforeCreate() {
|
||||||
// FIXME: async action in beforeCreate, might be not finished when component mounts
|
|
||||||
this.$store.dispatch('config/update')
|
|
||||||
.then(() => {
|
|
||||||
this.$store.dispatch('auth/checkAuth')
|
|
||||||
})
|
|
||||||
this.$store.dispatch('auth/checkAuth')
|
|
||||||
|
|
||||||
setLanguage()
|
setLanguage()
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
|
@ -125,29 +116,3 @@ export default defineComponent({
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '@/styles/global.scss';
|
@import '@/styles/global.scss';
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.offline {
|
|
||||||
background: url('@/assets/llama-nightscape.jpg') no-repeat center;
|
|
||||||
background-size: cover;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
.offline-message {
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
width: 100vw;
|
|
||||||
bottom: 5vh;
|
|
||||||
color: var(--white);
|
|
||||||
padding: 0 1rem;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--white);
|
|
||||||
font-weight: 700 !important;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,16 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="$store.commit('toggleMenu')"
|
@click="$store.commit('toggleMenu')"
|
||||||
class="menu-show-button"
|
class="menu-show-button"
|
||||||
@shortkey="() => $store.commit('toggleMenu')"
|
@shortkey="() => $store.commit('toggleMenu')"
|
||||||
v-shortkey="['ctrl', 'e']"
|
v-shortcut="'Control+e'"
|
||||||
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
|
:title="$t('keyboardShortcuts.toggleMenu')"
|
||||||
/>
|
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed} from 'vue'
|
import {computed} from 'vue'
|
||||||
import {store} from '@/store'
|
import {store} from '@/store'
|
||||||
|
|
||||||
const menuActive = computed(() => store.menuActive)
|
const menuActive = computed(() => store.menuActive)
|
||||||
|
@ -32,6 +33,7 @@ $size: $lineWidth + 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
$transformX: translateX(-50%);
|
$transformX: translateX(-50%);
|
||||||
|
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
|
|
@ -31,8 +31,7 @@
|
||||||
<a
|
<a
|
||||||
class="keyboard-shortcuts-button"
|
class="keyboard-shortcuts-button"
|
||||||
@click="showKeyboardShortcuts()"
|
@click="showKeyboardShortcuts()"
|
||||||
@shortkey="showKeyboardShortcuts()"
|
v-shortcut="'?'"
|
||||||
v-shortkey="['?']"
|
|
||||||
>
|
>
|
||||||
<icon icon="keyboard"/>
|
<icon icon="keyboard"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,37 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="no-auth-wrapper">
|
<no-auth-wrapper>
|
||||||
<div class="noauth-container">
|
<router-view/>
|
||||||
<Logo width="400" height="117" />
|
</no-auth-wrapper>
|
||||||
<div class="message is-info" v-if="motd !== ''">
|
|
||||||
<div class="message-header">
|
|
||||||
<p>{{ $t('misc.info') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="message-body">
|
|
||||||
{{ motd }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<router-view/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapState} from 'vuex'
|
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||||
|
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
||||||
import Logo from '@/components/home/Logo.vue'
|
|
||||||
|
|
||||||
import { saveLastVisited } from '@/helpers/saveLastVisited'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'contentNoAuth',
|
name: 'contentNoAuth',
|
||||||
components: { Logo },
|
components: {NoAuthWrapper},
|
||||||
computed: {
|
computed: {
|
||||||
routeName() {
|
routeName() {
|
||||||
return this.$route.name
|
return this.$route.name
|
||||||
},
|
},
|
||||||
...mapState({
|
|
||||||
motd: state => state.config.motd,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
routeName: {
|
routeName: {
|
||||||
|
@ -62,17 +45,3 @@ export default {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.no-auth-wrapper {
|
|
||||||
background: url('@/assets/llama.svg?url') no-repeat bottom left fixed var(--site-background);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.noauth-container {
|
|
||||||
max-width: 450px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -48,7 +48,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="menu namespaces-lists loader-container" :class="{'is-loading': loading}">
|
<aside class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
|
||||||
<template v-for="(n, nk) in namespaces" :key="n.id" >
|
<template v-for="(n, nk) in namespaces" :key="n.id" >
|
||||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
||||||
<span
|
<span
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
>
|
>
|
||||||
<template #item="{element: l}">
|
<template #item="{element: l}">
|
||||||
<li
|
<li
|
||||||
class="loader-container"
|
class="loader-container is-loading-small"
|
||||||
:class="{'is-loading': listUpdating[l.id]}"
|
:class="{'is-loading': listUpdating[l.id]}"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
|
@ -449,14 +449,6 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||||
&:hover :deep(.dropdown-trigger) {
|
&:hover :deep(.dropdown-trigger) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.loader-container.is-loading:after {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
top: calc(50% - .75rem);
|
|
||||||
left: calc(50% - .75rem);
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flip-list-move {
|
.flip-list-move {
|
||||||
|
@ -533,14 +525,6 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||||
padding-top: math.div($navbar-padding, 2);
|
padding-top: math.div($navbar-padding, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.loader-container.is-loading:after {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
top: calc(50% - .75rem);
|
|
||||||
left: calc(50% - .75rem);
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
color: var(--grey-400) !important;
|
color: var(--grey-400) !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
class="navbar main-theme is-fixed-top"
|
class="navbar main-theme is-fixed-top"
|
||||||
role="navigation"
|
role="navigation"
|
||||||
>
|
>
|
||||||
<router-link :to="{name: 'home'}" class="logo">
|
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||||
<Logo width="164" height="48" />
|
<Logo width="164" height="48"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<MenuButton class="menu-button" />
|
<MenuButton class="menu-button"/>
|
||||||
<div class="list-title" ref="listTitle" v-show="currentList.id">
|
<div class="list-title" ref="listTitle" v-show="currentList.id">
|
||||||
<template v-if="currentList.id">
|
<template v-if="currentList.id">
|
||||||
<h1
|
<h1
|
||||||
|
@ -26,8 +26,8 @@
|
||||||
<a
|
<a
|
||||||
@click="openQuickActions"
|
@click="openQuickActions"
|
||||||
class="trigger-button pr-0"
|
class="trigger-button pr-0"
|
||||||
@shortkey="openQuickActions"
|
v-shortcut="'Control+k'"
|
||||||
v-shortkey="['ctrl', 'k']"
|
:title="$t('keyboardShortcuts.quickSearch')"
|
||||||
>
|
>
|
||||||
<icon icon="search"/>
|
<icon icon="search"/>
|
||||||
</a>
|
</a>
|
||||||
|
@ -256,33 +256,33 @@ $vikunja-nav-logo-full-width: 164px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-title {
|
.list-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
$edit-icon-width: 1rem;
|
$edit-icon-width: 1rem;
|
||||||
|
|
||||||
@media screen and (min-width: $tablet) {
|
@media screen and (min-width: $tablet) {
|
||||||
// We need a fixed width for overflowing ellipsis to work
|
// We need a fixed width for overflowing ellipsis to work
|
||||||
--nav-username-width: 0;
|
--nav-username-width: 0;
|
||||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width} - #{$vikunja-nav-logo-full-width} - var(--nav-username-width));
|
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width} - #{$vikunja-nav-logo-full-width} - var(--nav-username-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $tablet) {
|
@media screen and (max-width: $tablet) {
|
||||||
// We need a fixed width for overflowing ellipsis to work
|
// We need a fixed width for overflowing ellipsis to work
|
||||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width});
|
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width});
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.dropdown-trigger) {
|
:deep(.dropdown-trigger) {
|
||||||
color: var(--grey-400);
|
color: var(--grey-400);
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,37 +1,49 @@
|
||||||
<template>
|
<template>
|
||||||
<transition name="fade">
|
<x-button
|
||||||
<filters
|
v-if="hasFilters"
|
||||||
v-if="visibleInternal"
|
type="secondary"
|
||||||
v-model="value"
|
@click="clearFilters"
|
||||||
ref="filters"
|
>
|
||||||
/>
|
{{ $t('filters.clear') }}
|
||||||
</transition>
|
</x-button>
|
||||||
|
<popup>
|
||||||
|
<template #trigger="{toggle}">
|
||||||
|
<x-button
|
||||||
|
@click.prevent.stop="toggle()"
|
||||||
|
type="secondary"
|
||||||
|
icon="filter"
|
||||||
|
>
|
||||||
|
{{ $t('filters.title') }}
|
||||||
|
</x-button>
|
||||||
|
</template>
|
||||||
|
<template #content="{isOpen}">
|
||||||
|
<filters
|
||||||
|
v-model="value"
|
||||||
|
ref="filters"
|
||||||
|
class="filter-popup"
|
||||||
|
:class="{'is-open': isOpen}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
import Filters from '@/components/list/partials/filters'
|
||||||
import Filters from '../../../components/list/partials/filters'
|
import {getDefaultParams} from '@/components/tasks/mixins/taskList'
|
||||||
|
import Popup from '@/components/misc/popup'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'filter-popup',
|
name: 'filter-popup',
|
||||||
components: {
|
components: {
|
||||||
|
Popup,
|
||||||
Filters,
|
Filters,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
visibleInternal: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
value: {
|
value: {
|
||||||
get() {
|
get() {
|
||||||
|
@ -41,34 +53,46 @@ export default {
|
||||||
this.$emit('update:modelValue', value)
|
this.$emit('update:modelValue', value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
hasFilters() {
|
||||||
mounted() {
|
// this.value also contains the page parameter which we don't want to include in filters
|
||||||
document.addEventListener('click', this.hidePopup)
|
// eslint-disable-next-line no-unused-vars
|
||||||
},
|
const {filter_by, filter_value, filter_comparator, filter_concat, s} = this.value
|
||||||
beforeUnmount() {
|
const def = {...getDefaultParams()}
|
||||||
document.removeEventListener('click', this.hidePopup)
|
|
||||||
|
const params = {filter_by, filter_value, filter_comparator, filter_concat, s}
|
||||||
|
const defaultParams = {
|
||||||
|
filter_by: def.filter_by,
|
||||||
|
filter_value: def.filter_value,
|
||||||
|
filter_comparator: def.filter_comparator,
|
||||||
|
filter_concat: def.filter_concat,
|
||||||
|
s: s ? def.s : undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(params) !== JSON.stringify(defaultParams)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
handler(value) {
|
handler(value) {
|
||||||
this.params = value
|
this.value = value
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
visible() {
|
|
||||||
this.visibleInternal = !this.visibleInternal
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hidePopup(e) {
|
clearFilters() {
|
||||||
if (!this.visibleInternal) {
|
this.value = {...getDefaultParams()}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
closeWhenClickedOutside(e, this.$refs.filters.$el, () => {
|
|
||||||
this.visibleInternal = false
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.filter-popup {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
margin: 2rem 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -458,15 +458,7 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let foundDone = false
|
this.filters.done = this.params.filter_by.some((f) => f === 'done') === false
|
||||||
this.params.filter_by.forEach((f, i) => {
|
|
||||||
if (f === 'done') {
|
|
||||||
foundDone = i
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (foundDone === false) {
|
|
||||||
this.filters.done = true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) {
|
async prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) {
|
||||||
if (filterName === null) {
|
if (filterName === null) {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<i18n-t keypath="apiConfig.signInOn">
|
<i18n-t keypath="apiConfig.signInOn">
|
||||||
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
|
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<br />
|
<br/>
|
||||||
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a>
|
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -46,9 +46,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { parseURL } from 'ufo'
|
import {parseURL} from 'ufo'
|
||||||
|
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||||
const API_DEFAULT_PORT = 3456
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'apiConfig',
|
name: 'apiConfig',
|
||||||
|
@ -71,128 +70,48 @@ export default {
|
||||||
return parseURL(this.apiUrl).host
|
return parseURL(this.apiUrl).host
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
configureOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
configureOpen: {
|
||||||
|
handler(value) {
|
||||||
|
this.configureApi = value
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setApiUrl() {
|
async setApiUrl() {
|
||||||
if (this.apiUrl === '') {
|
if (this.apiUrl === '') {
|
||||||
|
// Don't try to check and set an empty url
|
||||||
|
this.errorMsg = this.$t('apiConfig.urlRequired')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let urlToCheck = this.apiUrl
|
try {
|
||||||
|
const url = await checkAndSetApiUrl(this.apiUrl)
|
||||||
|
|
||||||
// Check if the url has an http prefix
|
if (url === '') {
|
||||||
if (
|
// If the config setter function could not figure out a url
|
||||||
!urlToCheck.startsWith('http://') &&
|
throw new Error('URL cannot be empty.')
|
||||||
!urlToCheck.startsWith('https://')
|
}
|
||||||
) {
|
|
||||||
urlToCheck = `http://${urlToCheck}`
|
// Set it + save it to local storage to save us the hoops
|
||||||
|
this.errorMsg = ''
|
||||||
|
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain})
|
||||||
|
this.configureApi = false
|
||||||
|
this.apiUrl = url
|
||||||
|
this.$emit('foundApi', this.apiUrl)
|
||||||
|
} catch (e) {
|
||||||
|
// Still not found, url is still invalid
|
||||||
|
this.successMsg = ''
|
||||||
|
this.errorMsg = this.$t('apiConfig.error', {domain: this.apiDomain})
|
||||||
}
|
}
|
||||||
|
|
||||||
urlToCheck = new URL(urlToCheck)
|
|
||||||
const origUrlToCheck = urlToCheck
|
|
||||||
|
|
||||||
const oldUrl = window.API_URL
|
|
||||||
window.API_URL = urlToCheck.toString()
|
|
||||||
|
|
||||||
// Check if the api is reachable at the provided url
|
|
||||||
this.$store
|
|
||||||
.dispatch('config/update')
|
|
||||||
.catch((e) => {
|
|
||||||
// Check if it is reachable at /api/v1 and http
|
|
||||||
if (
|
|
||||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
|
||||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
|
||||||
) {
|
|
||||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
|
||||||
window.API_URL = urlToCheck.toString()
|
|
||||||
return this.$store.dispatch('config/update')
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
// Check if it has a port and if not check if it is reachable at https
|
|
||||||
if (urlToCheck.protocol === 'http:') {
|
|
||||||
urlToCheck.protocol = 'https:'
|
|
||||||
window.API_URL = urlToCheck.toString()
|
|
||||||
return this.$store.dispatch('config/update')
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
// Check if it is reachable at /api/v1 and https
|
|
||||||
urlToCheck.pathname = origUrlToCheck.pathname
|
|
||||||
if (
|
|
||||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
|
||||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
|
||||||
) {
|
|
||||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
|
||||||
window.API_URL = urlToCheck.toString()
|
|
||||||
return this.$store.dispatch('config/update')
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
// Check if it is reachable at port API_DEFAULT_PORT and https
|
|
||||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
|
||||||
urlToCheck.protocol = 'https:'
|
|
||||||
urlToCheck.port = API_DEFAULT_PORT
|
|
||||||
window.API_URL = urlToCheck.toString()
|
|
||||||
return this.$store.dispatch('config/update')
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and https
|
|
||||||
urlToCheck.pathname = origUrlToCheck.pathname
|
|
||||||
if (
|
|
||||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
|
||||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
|
||||||
) {
|
|
||||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
|
||||||
window.API_URL = urlToCheck.toString()
|
|
||||||
return this.$store.dispatch('config/update')
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
// Check if it is reachable at port API_DEFAULT_PORT and http
|
|
||||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
|
||||||
urlToCheck.protocol = 'http:'
|
|
||||||
urlToCheck.port = API_DEFAULT_PORT
|
|
||||||
window.API_URL = urlToCheck.toString()
|
|
||||||
return this.$store.dispatch('config/update')
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and http
|
|
||||||
urlToCheck.pathname = origUrlToCheck.pathname
|
|
||||||
if (
|
|
||||||
!urlToCheck.pathname.endsWith('/api/v1') &&
|
|
||||||
!urlToCheck.pathname.endsWith('/api/v1/')
|
|
||||||
) {
|
|
||||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
|
||||||
window.API_URL = urlToCheck.toString()
|
|
||||||
return this.$store.dispatch('config/update')
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Still not found, url is still invalid
|
|
||||||
this.successMsg = ''
|
|
||||||
this.errorMsg = this.$t('apiConfig.error', {domain: this.apiDomain})
|
|
||||||
window.API_URL = oldUrl
|
|
||||||
})
|
|
||||||
.then((r) => {
|
|
||||||
if (typeof r !== 'undefined') {
|
|
||||||
// Set it + save it to local storage to save us the hoops
|
|
||||||
this.errorMsg = ''
|
|
||||||
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain})
|
|
||||||
localStorage.setItem('API_URL', window.API_URL)
|
|
||||||
this.configureApi = false
|
|
||||||
this.apiUrl = window.API_URL
|
|
||||||
this.$emit('foundApi', this.apiUrl)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -200,15 +119,15 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.api-config {
|
.api-config {
|
||||||
margin-bottom: .75rem;
|
margin-bottom: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.api-url-info {
|
.api-url-info {
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
span.url {
|
.url {
|
||||||
border-bottom: 1px dashed var(--primary);
|
border-bottom: 1px dashed var(--primary);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,72 +0,0 @@
|
||||||
<template>
|
|
||||||
<modal @close="close()">
|
|
||||||
<card class="has-no-shadow" :title="$t('keyboardShortcuts.title')">
|
|
||||||
<div class="message is-primary">
|
|
||||||
<div class="message-body">
|
|
||||||
{{ $t('keyboardShortcuts.allPages') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<strong>{{ $t('keyboardShortcuts.toggleMenu') }}</strong>
|
|
||||||
<shortcut :keys="['ctrl', 'e']"/>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>{{ $t('keyboardShortcuts.quickSearch') }}</strong>
|
|
||||||
<shortcut :keys="['ctrl', 'k']"/>
|
|
||||||
</p>
|
|
||||||
<h3>{{ $t('list.kanban.title') }}</h3>
|
|
||||||
<div class="message is-primary" v-if="$route.name === 'list.kanban'">
|
|
||||||
<div class="message-body">
|
|
||||||
{{ $t('keyboardShortcuts.currentPageOnly') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<strong>{{ $t('keyboardShortcuts.task.done') }}</strong>
|
|
||||||
<shortcut :keys="['ctrl', 'click']"/>
|
|
||||||
</p>
|
|
||||||
<h3>{{ $t('keyboardShortcuts.task.title') }}</h3>
|
|
||||||
<div
|
|
||||||
class="message is-primary"
|
|
||||||
v-if="$route.name === 'task.detail' || $route.name === 'task.list.detail' || $route.name === 'task.gantt.detail' || $route.name === 'task.kanban.detail' || $route.name === 'task.detail'">
|
|
||||||
<div class="message-body">
|
|
||||||
{{ $t('keyboardShortcuts.currentPageOnly') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<strong>{{ $t('keyboardShortcuts.task.assign') }}</strong>
|
|
||||||
<shortcut :keys="['a']"/>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>{{ $t('keyboardShortcuts.task.labels') }}</strong>
|
|
||||||
<shortcut :keys="['l']"/>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>{{ $t('keyboardShortcuts.task.dueDate') }}</strong>
|
|
||||||
<shortcut :keys="['d']"/>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>{{ $t('keyboardShortcuts.task.attachment') }}</strong>
|
|
||||||
<shortcut :keys="['f']"/>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>{{ $t('keyboardShortcuts.task.related') }}</strong>
|
|
||||||
<shortcut :keys="['r']"/>
|
|
||||||
</p>
|
|
||||||
</card>
|
|
||||||
</modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
|
|
||||||
import Shortcut from '@/components/misc/shortcut.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'keyboard-shortcuts',
|
|
||||||
components: {Shortcut},
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
54
src/components/misc/keyboard-shortcuts/index.vue
Normal file
54
src/components/misc/keyboard-shortcuts/index.vue
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<modal @close="close()">
|
||||||
|
<card class="has-background-white has-no-shadow" :title="$t('keyboardShortcuts.title')">
|
||||||
|
<template v-for="(s, i) in shortcuts" :key="i">
|
||||||
|
<h3>{{ $t(s.title) }}</h3>
|
||||||
|
|
||||||
|
<div class="message is-primary">
|
||||||
|
<div class="message-body">
|
||||||
|
{{
|
||||||
|
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<template v-for="(sc, si) in s.shortcuts" :key="si">
|
||||||
|
<dt>{{ $t(sc.title) }}</dt>
|
||||||
|
<shortcut
|
||||||
|
is="dd"
|
||||||
|
:keys="sc.keys"
|
||||||
|
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
</template>
|
||||||
|
</card>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
|
||||||
|
import Shortcut from '@/components/misc/shortcut.vue'
|
||||||
|
import {KEYBOARD_SHORTCUTS} from './shortcuts'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'keyboard-shortcuts',
|
||||||
|
components: {Shortcut},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
shortcuts: KEYBOARD_SHORTCUTS,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
88
src/components/misc/keyboard-shortcuts/shortcuts.js
Normal file
88
src/components/misc/keyboard-shortcuts/shortcuts.js
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||||
|
|
||||||
|
const ctrl = isAppleDevice() ? '⌘' : 'ctrl'
|
||||||
|
|
||||||
|
export const KEYBOARD_SHORTCUTS = [
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.general',
|
||||||
|
available: () => null,
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.toggleMenu',
|
||||||
|
keys: [ctrl, 'e'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.quickSearch',
|
||||||
|
keys: [ctrl, 'k'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'list.kanban.title',
|
||||||
|
available: (route) => route.name === 'list.kanban',
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.done',
|
||||||
|
keys: [ctrl, 'click'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.list.title',
|
||||||
|
available: (route) => route.name.startsWith('list.'),
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.list.switchToListView',
|
||||||
|
keys: ['g', 'l'],
|
||||||
|
combination: 'then',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.list.switchToGanttView',
|
||||||
|
keys: ['g', 'g'],
|
||||||
|
combination: 'then',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.list.switchToTableView',
|
||||||
|
keys: ['g', 't'],
|
||||||
|
combination: 'then',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.list.switchToKanbanView',
|
||||||
|
keys: ['g', 'k'],
|
||||||
|
combination: 'then',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.title',
|
||||||
|
available: (route) => [
|
||||||
|
'task.detail',
|
||||||
|
'task.list.detail',
|
||||||
|
'task.gantt.detail',
|
||||||
|
'task.kanban.detail',
|
||||||
|
'task.detail',
|
||||||
|
].includes(route.name),
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.assign',
|
||||||
|
keys: ['a'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.labels',
|
||||||
|
keys: ['l'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.dueDate',
|
||||||
|
keys: ['d'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.attachment',
|
||||||
|
keys: ['f'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'keyboardShortcuts.task.related',
|
||||||
|
keys: ['r'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
39
src/components/misc/no-auth-wrapper.vue
Normal file
39
src/components/misc/no-auth-wrapper.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="no-auth-wrapper">
|
||||||
|
<div class="noauth-container">
|
||||||
|
<Logo width="400" height="117" />
|
||||||
|
<div class="message is-info" v-if="motd !== ''">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{{ $t('misc.info') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
{{ motd }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Logo from '@/components/home/Logo.vue'
|
||||||
|
import {useStore} from 'vuex'
|
||||||
|
import {computed} from 'vue'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const motd = computed(() => store.state.config.motd)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.no-auth-wrapper {
|
||||||
|
background: url('@/assets/llama.svg') no-repeat bottom left fixed $light-background;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noauth-container {
|
||||||
|
max-width: 450px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
54
src/components/misc/popup.vue
Normal file
54
src/components/misc/popup.vue
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<slot name="trigger" :isOpen="open" :toggle="toggle"></slot>
|
||||||
|
<div class="popup" :class="{'is-open': open}" ref="popup">
|
||||||
|
<slot name="content" :isOpen="open"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||||
|
import {onBeforeUnmount, onMounted, ref} from 'vue'
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
const popup = ref(null)
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
open.value = !open.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePopup(e) {
|
||||||
|
if (!open.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we actually want to use popup.$el, not its value.
|
||||||
|
// eslint-disable-next-line vue/no-ref-as-operand
|
||||||
|
closeWhenClickedOutside(e, popup.value, () => {
|
||||||
|
open.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', hidePopup)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', hidePopup)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.popup {
|
||||||
|
transition: opacity $transition;
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
opacity: 1;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
141
src/components/misc/ready.vue
Normal file
141
src/components/misc/ready.vue
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
<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-message">
|
||||||
|
<h1>{{ $t('offline.title') }}</h1>
|
||||||
|
<p>{{ $t('offline.text') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else-if="ready">
|
||||||
|
<slot/>
|
||||||
|
</template>
|
||||||
|
<section v-else-if="error !== ''">
|
||||||
|
<no-auth-wrapper>
|
||||||
|
<card>
|
||||||
|
<p v-if="error === errorNoApiUrl">
|
||||||
|
{{ $t('ready.noApiUrlConfigured') }}
|
||||||
|
</p>
|
||||||
|
<div class="notification is-danger" v-else>
|
||||||
|
<p>
|
||||||
|
{{ $t('ready.errorOccured') }}<br/>
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ $t('ready.checkApiUrl') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<api-config :configure-open="true" @found-api="load"/>
|
||||||
|
</card>
|
||||||
|
</no-auth-wrapper>
|
||||||
|
</section>
|
||||||
|
<transition name="fade">
|
||||||
|
<section class="vikunja-loading" v-if="showLoading">
|
||||||
|
<img alt="Vikunja" :src="logoUrl" width="100" height="100"/>
|
||||||
|
<p>
|
||||||
|
<span class="loader-container is-loading-small is-loading"></span>
|
||||||
|
{{ $t('ready.loading') }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import logoUrl from '@/assets/logo.svg'
|
||||||
|
import ApiConfig from '@/components/misc/api-config'
|
||||||
|
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ready',
|
||||||
|
components: {
|
||||||
|
NoAuthWrapper,
|
||||||
|
ApiConfig,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
logoUrl,
|
||||||
|
error: '',
|
||||||
|
errorNoApiUrl: ERROR_NO_API_URL,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
ready() {
|
||||||
|
return this.$store.state.vikunjaReady
|
||||||
|
},
|
||||||
|
showLoading() {
|
||||||
|
return !this.ready && this.error === ''
|
||||||
|
},
|
||||||
|
...mapState([
|
||||||
|
'online',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load() {
|
||||||
|
this.$store.dispatch('loadApp')
|
||||||
|
.catch(e => {
|
||||||
|
this.error = e
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vikunja-loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
flex-direction: column;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: $grey-100;
|
||||||
|
z-index: 99;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-container {
|
||||||
|
margin-right: 1rem;
|
||||||
|
|
||||||
|
&.is-loading::after {
|
||||||
|
border-left-color: $grey-400;
|
||||||
|
border-bottom-color: $grey-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline {
|
||||||
|
background: url('@/assets/llama-nightscape.jpg') no-repeat center;
|
||||||
|
background-size: cover;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-message {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 100vw;
|
||||||
|
bottom: 5vh;
|
||||||
|
color: $white;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
color: $white;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="shortcuts">
|
<component :is="is" class="shortcuts">
|
||||||
<template v-for="(k, i) in keys" :key="i">
|
<template v-for="(k, i) in keys" :key="i">
|
||||||
<kbd>{{ k }}</kbd>
|
<kbd>{{ k }}</kbd>
|
||||||
<span v-if="i < keys.length - 1">+</span>
|
<span v-if="i < keys.length - 1">{{ combination }}</span>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -15,6 +15,14 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
combination: {
|
||||||
|
type: String,
|
||||||
|
default: '+',
|
||||||
|
},
|
||||||
|
is: {
|
||||||
|
type: String,
|
||||||
|
default: 'div',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,8 +12,7 @@
|
||||||
class="modal-container"
|
class="modal-container"
|
||||||
:class="{'has-overflow': overflow}"
|
:class="{'has-overflow': overflow}"
|
||||||
@click.self.prevent.stop="$emit('close')"
|
@click.self.prevent.stop="$emit('close')"
|
||||||
@shortkey="$emit('close')"
|
v-shortcut="'Escape'"
|
||||||
v-shortkey="['esc']"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-content"
|
class="modal-content"
|
||||||
|
|
|
@ -62,7 +62,10 @@ import TeamModel from '@/models/team'
|
||||||
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
|
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
|
||||||
import ListModel from '@/models/list'
|
import ListModel from '@/models/list'
|
||||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||||
import {getHistory} from '../../modules/listHistory'
|
import {getHistory} from '@/modules/listHistory'
|
||||||
|
import {parseTaskText, PrefixMode} from '@/modules/parseTaskText'
|
||||||
|
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
|
||||||
|
import {PREFIXES} from '@/modules/parseTaskText'
|
||||||
|
|
||||||
const TYPE_LIST = 'list'
|
const TYPE_LIST = 'list'
|
||||||
const TYPE_TASK = 'task'
|
const TYPE_TASK = 'task'
|
||||||
|
@ -107,11 +110,6 @@ export default {
|
||||||
results() {
|
results() {
|
||||||
let lists = []
|
let lists = []
|
||||||
if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) {
|
if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) {
|
||||||
let query = this.query
|
|
||||||
if (this.searchMode === SEARCH_MODE_LISTS) {
|
|
||||||
query = query.substr(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ncache = {}
|
const ncache = {}
|
||||||
|
|
||||||
const history = getHistory()
|
const history = getHistory()
|
||||||
|
@ -122,25 +120,31 @@ export default {
|
||||||
}),
|
}),
|
||||||
...Object.values(this.$store.state.lists)])]
|
...Object.values(this.$store.state.lists)])]
|
||||||
|
|
||||||
lists = (allLists.filter(l => {
|
const {list} = this.parsedQuery
|
||||||
if (typeof l === 'undefined' || l === null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (l.isArchived) {
|
if (list === null) {
|
||||||
return false
|
lists = []
|
||||||
}
|
} else {
|
||||||
|
lists = allLists.filter(l => {
|
||||||
|
if (typeof l === 'undefined' || l === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof ncache[l.namespaceId] === 'undefined') {
|
if (l.isArchived) {
|
||||||
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ncache[l.namespaceId].isArchived) {
|
if (typeof ncache[l.namespaceId] === 'undefined') {
|
||||||
return false
|
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return l.title.toLowerCase().includes(query.toLowerCase())
|
if (ncache[l.namespaceId].isArchived) {
|
||||||
}) ?? [])
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.title.toLowerCase().includes(list.toLowerCase())
|
||||||
|
}) ?? []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmds = this.availableCmds
|
const cmds = this.availableCmds
|
||||||
|
@ -207,7 +211,9 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.$t('quickActions.hint')
|
const prefixes = PREFIXES[getQuickAddMagicMode()] ?? PREFIXES[PrefixMode.Default]
|
||||||
|
|
||||||
|
return this.$t('quickActions.hint', prefixes)
|
||||||
},
|
},
|
||||||
currentList() {
|
currentList() {
|
||||||
return Object.keys(this.$store.state[CURRENT_LIST]).length === 0 ? null : this.$store.state[CURRENT_LIST]
|
return Object.keys(this.$store.state[CURRENT_LIST]).length === 0 ? null : this.$store.state[CURRENT_LIST]
|
||||||
|
@ -236,18 +242,23 @@ export default {
|
||||||
|
|
||||||
return cmds
|
return cmds
|
||||||
},
|
},
|
||||||
|
parsedQuery() {
|
||||||
|
return parseTaskText(this.query, getQuickAddMagicMode())
|
||||||
|
},
|
||||||
searchMode() {
|
searchMode() {
|
||||||
if (this.query === '') {
|
if (this.query === '') {
|
||||||
return SEARCH_MODE_ALL
|
return SEARCH_MODE_ALL
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.query.startsWith('#')) {
|
const {text, list, labels, assignees} = this.parsedQuery
|
||||||
|
|
||||||
|
if (assignees.length === 0 && text !== '') {
|
||||||
return SEARCH_MODE_TASKS
|
return SEARCH_MODE_TASKS
|
||||||
}
|
}
|
||||||
if (this.query.startsWith('*')) {
|
if (assignees.length === 0 && list !== null && text === '' && labels.length === 0) {
|
||||||
return SEARCH_MODE_LISTS
|
return SEARCH_MODE_LISTS
|
||||||
}
|
}
|
||||||
if (this.query.startsWith('@')) {
|
if (assignees.length > 0 && list === null && text === '' && labels.length === 0) {
|
||||||
return SEARCH_MODE_TEAMS
|
return SEARCH_MODE_TEAMS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,12 +279,7 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = this.query
|
if (this.selectedCmd !== null) {
|
||||||
if (this.searchMode === SEARCH_MODE_TASKS) {
|
|
||||||
query = query.substr(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query === '' || this.selectedCmd !== null) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,8 +288,35 @@ export default {
|
||||||
this.taskSearchTimeout = null
|
this.taskSearchTimeout = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {text, list, labels} = this.parsedQuery
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
s: text,
|
||||||
|
filter_by: [],
|
||||||
|
filter_value: [],
|
||||||
|
filter_comparator: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list !== null) {
|
||||||
|
const l = this.$store.getters['lists/findListByExactname'](list)
|
||||||
|
if (l !== null) {
|
||||||
|
params.filter_by.push('list_id')
|
||||||
|
params.filter_value.push(l.id)
|
||||||
|
params.filter_comparator.push('equals')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labels.length > 0) {
|
||||||
|
const labelIds = this.$store.getters['labels/getLabelsByExactTitles'](labels).map(l => l.id)
|
||||||
|
if (labelIds.length > 0) {
|
||||||
|
params.filter_by.push('labels')
|
||||||
|
params.filter_value.push(labelIds.join())
|
||||||
|
params.filter_comparator.push('in')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.taskSearchTimeout = setTimeout(async () => {
|
this.taskSearchTimeout = setTimeout(async () => {
|
||||||
const r = await this.taskService.getAll({}, {s: query})
|
const r = await this.taskService.getAll({}, params)
|
||||||
this.foundTasks = r.map(t => {
|
this.foundTasks = r.map(t => {
|
||||||
t.type = TYPE_TASK
|
t.type = TYPE_TASK
|
||||||
const list = this.$store.getters['lists/getListById'](t.listId)
|
const list = this.$store.getters['lists/getListById'](t.listId)
|
||||||
|
@ -301,12 +334,7 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = this.query
|
if (this.query === '' || this.selectedCmd !== null) {
|
||||||
if (this.searchMode === SEARCH_MODE_TEAMS) {
|
|
||||||
query = query.substr(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query === '' || this.selectedCmd !== null) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,11 +343,14 @@ export default {
|
||||||
this.teamSearchTimeout = null
|
this.teamSearchTimeout = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {assignees} = this.parsedQuery
|
||||||
|
|
||||||
this.teamSearchTimeout = setTimeout(async () => {
|
this.teamSearchTimeout = setTimeout(async () => {
|
||||||
const r = await this.teamService.getAll({}, {s: query})
|
const teamSearchPromises = assignees.map((t) => this.teamService.getAll({}, {s: t}))
|
||||||
this.foundTeams = r.map(t => {
|
const teamsResult = await Promise.all(teamSearchPromises)
|
||||||
t.title = t.name
|
this.foundTeams = teamsResult.flatMap(team => {
|
||||||
return t
|
team.title = team.name
|
||||||
|
return team
|
||||||
})
|
})
|
||||||
}, 150)
|
}, 150)
|
||||||
},
|
},
|
||||||
|
@ -348,7 +379,7 @@ export default {
|
||||||
this.doAction(this.results[0].type, this.results[0].items[0])
|
this.doAction(this.results[0].type, this.results[0].items[0])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectedCmd === null) {
|
if (this.selectedCmd === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
>
|
>
|
||||||
{{ $t('filters.title') }}
|
{{ $t('filters.title') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
|
<filter-popup
|
||||||
|
:visible="showTaskFilter"
|
||||||
|
v-model="params"
|
||||||
|
@update:modelValue="loadTasks()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<filter-popup
|
|
||||||
:visible="showTaskFilter"
|
|
||||||
v-model="params"
|
|
||||||
@update:modelValue="loadTasks()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dates">
|
<div class="dates">
|
||||||
<template v-for="(y, yk) in days" :key="yk + 'year'">
|
<template v-for="(y, yk) in days" :key="yk + 'year'">
|
||||||
|
@ -347,7 +347,7 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let newTask = { ...taskDragged }
|
let newTask = {...taskDragged}
|
||||||
|
|
||||||
const didntHaveDates = newTask.startDate === null ? true : false
|
const didntHaveDates = newTask.startDate === null ? true : false
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import TaskCollectionService from '@/services/taskCollection'
|
import TaskCollectionService from '@/services/taskCollection'
|
||||||
|
|
||||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||||
const DEFAULT_PARAMS = {
|
export const getDefaultParams = () => ({
|
||||||
sort_by: ['position', 'id'],
|
sort_by: ['position', 'id'],
|
||||||
order_by: ['asc', 'desc'],
|
order_by: ['asc', 'desc'],
|
||||||
filter_by: ['done'],
|
filter_by: ['done'],
|
||||||
filter_value: ['false'],
|
filter_value: ['false'],
|
||||||
filter_comparator: ['equals'],
|
filter_comparator: ['equals'],
|
||||||
filter_concat: 'and',
|
filter_concat: 'and',
|
||||||
}
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||||
|
@ -26,7 +26,7 @@ export default {
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
|
||||||
showTaskFilter: false,
|
showTaskFilter: false,
|
||||||
params: DEFAULT_PARAMS,
|
params: {...getDefaultParams()},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -94,7 +94,7 @@ export default {
|
||||||
this.initTasks(page, search)
|
this.initTasks(page, search)
|
||||||
},
|
},
|
||||||
loadTasksOnSavedFilter() {
|
loadTasksOnSavedFilter() {
|
||||||
if(typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
|
if (typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
|
||||||
this.loadTasks(1, '', null, true)
|
this.loadTasks(1, '', null, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
:placeholder="$t('task.relation.searchPlaceholder')"
|
:placeholder="$t('task.relation.searchPlaceholder')"
|
||||||
@search="findTasks"
|
@search="findTasks"
|
||||||
:loading="taskService.loading"
|
:loading="taskService.loading"
|
||||||
:search-results="foundTasks"
|
:search-results="mappedFoundTasks"
|
||||||
label="title"
|
label="title"
|
||||||
v-model="newTaskRelationTask"
|
v-model="newTaskRelationTask"
|
||||||
:creatable="true"
|
:creatable="true"
|
||||||
|
@ -41,8 +41,17 @@
|
||||||
<span
|
<span
|
||||||
class="different-list"
|
class="different-list"
|
||||||
v-if="props.option.listId !== listId"
|
v-if="props.option.listId !== listId"
|
||||||
v-tooltip="$t('task.relation.differentList')">
|
>
|
||||||
{{ $store.getters['lists/getListById'](props.option.listId) === null ? '' : $store.getters['lists/getListById'](props.option.listId).title }} >
|
<span
|
||||||
|
v-if="props.option.differentNamespace !== null"
|
||||||
|
v-tooltip="$t('task.relation.differentNamespace')">
|
||||||
|
{{ props.option.differentNamespace }} >
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="props.option.differentList !== null"
|
||||||
|
v-tooltip="$t('task.relation.differentList')">
|
||||||
|
{{ props.option.differentList }} >
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{{ props.option.title }}
|
{{ props.option.title }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -70,33 +79,36 @@
|
||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
|
|
||||||
<div :key="kind" class="related-tasks" v-for="(rts, kind ) in relatedTasks">
|
<div :key="rts.kind" class="related-tasks" v-for="rts in mappedRelatedTasks">
|
||||||
<template v-if="rts.length > 0">
|
<span class="title">{{ rts.title }}</span>
|
||||||
<span class="title">{{ relationKindTitle(kind, rts.length) }}</span>
|
<div class="tasks">
|
||||||
<div class="tasks noborder">
|
<div :key="t.id" class="task" v-for="t in rts.tasks">
|
||||||
<div :key="t.id" class="task" v-for="t in rts.filter(t => t)">
|
<router-link :to="{ name: $route.name, params: { id: t.id } }" :class="{ 'done': t.done}">
|
||||||
<router-link :to="{ name: $route.name, params: { id: t.id } }">
|
<span
|
||||||
<span :class="{ 'done': t.done}" class="tasktext">
|
class="different-list"
|
||||||
<span
|
v-if="t.listId !== listId"
|
||||||
class="different-list"
|
>
|
||||||
v-if="t.listId !== listId"
|
<span
|
||||||
v-tooltip="$t('task.relation.differentList')">
|
v-if="t.differentNamespace !== null"
|
||||||
{{
|
v-tooltip="$t('task.relation.differentNamespace')">
|
||||||
$store.getters['lists/getListById'](t.listId) === null ? '' : $store.getters['lists/getListById'](t.listId).title
|
{{ t.differentNamespace }} >
|
||||||
}} >
|
|
||||||
</span>
|
|
||||||
{{ t.title }}
|
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
<span
|
||||||
<a
|
v-if="t.differentList !== null"
|
||||||
@click="() => {showDeleteModal = true; relationToDelete = {relationKind: kind, otherTaskId: t.id}}"
|
v-tooltip="$t('task.relation.differentList')">
|
||||||
class="remove"
|
{{ t.differentList }} >
|
||||||
v-if="editEnabled">
|
</span>
|
||||||
<icon icon="trash-alt"/>
|
</span>
|
||||||
</a>
|
{{ t.title }}
|
||||||
</div>
|
</router-link>
|
||||||
|
<a
|
||||||
|
@click="() => {showDeleteModal = true; relationToDelete = {relationKind: rts.kind, otherTaskId: t.id}}"
|
||||||
|
class="remove"
|
||||||
|
v-if="editEnabled">
|
||||||
|
<icon icon="trash-alt"/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="none" v-if="showNoRelationsNotice && Object.keys(relatedTasks).length === 0">
|
<p class="none" v-if="showNoRelationsNotice && Object.keys(relatedTasks).length === 0">
|
||||||
{{ $t('task.relation.noneYet') }}
|
{{ $t('task.relation.noneYet') }}
|
||||||
|
@ -110,10 +122,10 @@
|
||||||
v-if="showDeleteModal"
|
v-if="showDeleteModal"
|
||||||
>
|
>
|
||||||
<template #header><span>{{ $t('task.relation.delete') }}</span></template>
|
<template #header><span>{{ $t('task.relation.delete') }}</span></template>
|
||||||
|
|
||||||
<template #text>
|
<template #text>
|
||||||
<p>{{ $t('task.relation.deleteText1') }}<br/>
|
<p>{{ $t('task.relation.deleteText1') }}<br/>
|
||||||
<strong>{{ $t('task.relation.deleteText2') }}</strong></p>
|
<strong>{{ $t('task.relation.deleteText2') }}</strong></p>
|
||||||
</template>
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</transition>
|
</transition>
|
||||||
|
@ -183,6 +195,19 @@ export default {
|
||||||
showCreate() {
|
showCreate() {
|
||||||
return Object.keys(this.relatedTasks).length === 0 || this.showNewRelationForm
|
return Object.keys(this.relatedTasks).length === 0 || this.showNewRelationForm
|
||||||
},
|
},
|
||||||
|
namespace() {
|
||||||
|
return this.$store.getters['namespaces/getListAndNamespaceById'](this.listId, true)?.namespace
|
||||||
|
},
|
||||||
|
mappedRelatedTasks() {
|
||||||
|
return Object.entries(this.relatedTasks).map(([kind, tasks]) => ({
|
||||||
|
title: this.$tc(`task.relation.kinds.${kind}`, tasks.length),
|
||||||
|
tasks: this.mapRelatedTasks(tasks),
|
||||||
|
kind,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
mappedFoundTasks() {
|
||||||
|
return this.mapRelatedTasks(this.foundTasks.filter(t => t.id !== this.taskId))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async findTasks(query) {
|
async findTasks(query) {
|
||||||
|
@ -217,15 +242,14 @@ export default {
|
||||||
try {
|
try {
|
||||||
await this.taskRelationService.delete(rel)
|
await this.taskRelationService.delete(rel)
|
||||||
|
|
||||||
Object.entries(this.relatedTasks).some(([relationKind, t]) => {
|
const kind = this.relationToDelete.relationKind
|
||||||
const found = typeof this.relatedTasks[relationKind][t] !== 'undefined' &&
|
for (const t in this.relatedTasks[kind]) {
|
||||||
this.relatedTasks[relationKind][t].id === this.relationToDelete.otherTaskId &&
|
if (this.relatedTasks[kind][t].id === this.relationToDelete.otherTaskId) {
|
||||||
relationKind === this.relationToDelete.relationKind
|
this.relatedTasks[kind].splice(t, 1)
|
||||||
if (!found) return false
|
|
||||||
|
|
||||||
this.relatedTasks[relationKind].splice(t, 1)
|
break
|
||||||
return true
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
this.saved = true
|
this.saved = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -245,13 +269,34 @@ export default {
|
||||||
relationKindTitle(kind, length) {
|
relationKindTitle(kind, length) {
|
||||||
return this.$tc(`task.relation.kinds.${kind}`, length)
|
return this.$tc(`task.relation.kinds.${kind}`, length)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mapRelatedTasks(tasks) {
|
||||||
|
return tasks
|
||||||
|
.map(task => {
|
||||||
|
// by doing this here once we can save a lot of duplicate calls in the template
|
||||||
|
const {
|
||||||
|
list,
|
||||||
|
namespace,
|
||||||
|
} = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
differentNamespace:
|
||||||
|
(namespace !== null &&
|
||||||
|
namespace.id !== this.namespace.id &&
|
||||||
|
namespace?.title) || null,
|
||||||
|
differentList:
|
||||||
|
(list !== null &&
|
||||||
|
task.listId !== this.listId &&
|
||||||
|
list?.title) || null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$remove-icon-width: 24px;
|
|
||||||
|
|
||||||
.add-task-relation-button {
|
.add-task-relation-button {
|
||||||
margin-top: -3rem;
|
margin-top: -3rem;
|
||||||
|
|
||||||
|
@ -264,71 +309,55 @@ $remove-icon-width: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-relations {
|
.different-list {
|
||||||
&.is-narrow .columns {
|
color: var(--grey-500);
|
||||||
display: block;
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.column {
|
.title {
|
||||||
width: 100%;
|
font-size: 1rem;
|
||||||
}
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.different-list {
|
.task {
|
||||||
color: var(--grey-500);
|
display: flex;
|
||||||
width: auto;
|
flex-wrap: wrap;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
padding: .75rem;
|
||||||
|
transition: background-color $transition;
|
||||||
|
border-radius: $radius;
|
||||||
|
|
||||||
.related-tasks {
|
&:hover {
|
||||||
.title {
|
background-color: $grey-200;
|
||||||
font-size: 1rem;
|
}
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tasks {
|
a {
|
||||||
margin: 0;
|
color: var(--text);
|
||||||
|
transition: color ease $transition-duration;
|
||||||
|
|
||||||
a:not(.remove) {
|
&:hover {
|
||||||
width: calc(100% - #{$remove-icon-width});
|
color: var(--grey-900);
|
||||||
}
|
|
||||||
|
|
||||||
.task .tasktext {
|
|
||||||
width: calc(100% - .25rem); // Magic .25rem extra space
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove {
|
|
||||||
width: $remove-icon-width;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: .4rem;
|
|
||||||
transition: background-color $transition;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: $radius;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--text);
|
|
||||||
transition: color ease $transition-duration;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--grey-900);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.none {
|
.remove {
|
||||||
font-style: italic;
|
text-align: center;
|
||||||
text-align: center;
|
color: var(--danger);
|
||||||
}
|
opacity: 0;
|
||||||
|
transition: opacity $transition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-tasks:hover .tasks .task .remove {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.none {
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.multiselect .search-results button) {
|
||||||
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
17
src/directives/shortcut.ts
Normal file
17
src/directives/shortcut.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import {Directive} from 'vue'
|
||||||
|
import {install, uninstall} from '@github/hotkey'
|
||||||
|
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||||
|
|
||||||
|
const directive: Directive = {
|
||||||
|
mounted(el, {value}) {
|
||||||
|
if (isAppleDevice() && value.includes('Control')) {
|
||||||
|
value = value.replace('Control', 'Meta')
|
||||||
|
}
|
||||||
|
install(el, value)
|
||||||
|
},
|
||||||
|
beforeUnmount(el) {
|
||||||
|
uninstall(el)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default directive
|
118
src/helpers/checkAndSetApiUrl.ts
Normal file
118
src/helpers/checkAndSetApiUrl.ts
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import {store} from '@/store'
|
||||||
|
|
||||||
|
const API_DEFAULT_PORT = '3456'
|
||||||
|
|
||||||
|
export const ERROR_NO_API_URL = 'noApiUrlProvided'
|
||||||
|
|
||||||
|
const updateConfig = () => store.dispatch('config/update')
|
||||||
|
|
||||||
|
export const checkAndSetApiUrl = (url: string): Promise<string> => {
|
||||||
|
// Check if the url has an http prefix
|
||||||
|
if (
|
||||||
|
!url.startsWith('http://') &&
|
||||||
|
!url.startsWith('https://')
|
||||||
|
) {
|
||||||
|
url = `http://${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlToCheck: URL = new URL(url)
|
||||||
|
const origUrlToCheck = urlToCheck
|
||||||
|
|
||||||
|
const oldUrl = window.API_URL
|
||||||
|
window.API_URL = urlToCheck.toString()
|
||||||
|
|
||||||
|
// Check if the api is reachable at the provided url
|
||||||
|
return updateConfig()
|
||||||
|
.catch(e => {
|
||||||
|
// Check if it is reachable at /api/v1 and http
|
||||||
|
if (
|
||||||
|
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||||
|
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||||
|
) {
|
||||||
|
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||||
|
window.API_URL = urlToCheck.toString()
|
||||||
|
return updateConfig()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// Check if it has a port and if not check if it is reachable at https
|
||||||
|
if (urlToCheck.protocol === 'http:') {
|
||||||
|
urlToCheck.protocol = 'https:'
|
||||||
|
window.API_URL = urlToCheck.toString()
|
||||||
|
return updateConfig()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// Check if it is reachable at /api/v1 and https
|
||||||
|
urlToCheck.pathname = origUrlToCheck.pathname
|
||||||
|
if (
|
||||||
|
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||||
|
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||||
|
) {
|
||||||
|
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||||
|
window.API_URL = urlToCheck.toString()
|
||||||
|
return updateConfig()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// Check if it is reachable at port API_DEFAULT_PORT and https
|
||||||
|
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||||
|
urlToCheck.protocol = 'https:'
|
||||||
|
urlToCheck.port = API_DEFAULT_PORT
|
||||||
|
window.API_URL = urlToCheck.toString()
|
||||||
|
return updateConfig()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and https
|
||||||
|
urlToCheck.pathname = origUrlToCheck.pathname
|
||||||
|
if (
|
||||||
|
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||||
|
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||||
|
) {
|
||||||
|
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||||
|
window.API_URL = urlToCheck.toString()
|
||||||
|
return updateConfig()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// Check if it is reachable at port API_DEFAULT_PORT and http
|
||||||
|
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||||
|
urlToCheck.protocol = 'http:'
|
||||||
|
urlToCheck.port = API_DEFAULT_PORT
|
||||||
|
window.API_URL = urlToCheck.toString()
|
||||||
|
return updateConfig()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and http
|
||||||
|
urlToCheck.pathname = origUrlToCheck.pathname
|
||||||
|
if (
|
||||||
|
!urlToCheck.pathname.endsWith('/api/v1') &&
|
||||||
|
!urlToCheck.pathname.endsWith('/api/v1/')
|
||||||
|
) {
|
||||||
|
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||||
|
window.API_URL = urlToCheck.toString()
|
||||||
|
return updateConfig()
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
window.API_URL = oldUrl
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
if (typeof r !== 'undefined') {
|
||||||
|
localStorage.setItem('API_URL', window.API_URL)
|
||||||
|
return window.API_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(ERROR_NO_API_URL)
|
||||||
|
})
|
||||||
|
}
|
10
src/helpers/isAppleDevice.ts
Normal file
10
src/helpers/isAppleDevice.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export const isAppleDevice = (): Boolean => {
|
||||||
|
return navigator.userAgent.includes('Mac') || [
|
||||||
|
'iPad Simulator',
|
||||||
|
'iPhone Simulator',
|
||||||
|
'iPod Simulator',
|
||||||
|
'iPad',
|
||||||
|
'iPhone',
|
||||||
|
'iPod',
|
||||||
|
].includes(navigator.platform)
|
||||||
|
}
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Uloženo!",
|
"saved": "Uloženo!",
|
||||||
"default": "Výchozí",
|
"default": "Výchozí",
|
||||||
"close": "Zavřít",
|
"close": "Zavřít",
|
||||||
"download": "Stáhnout"
|
"download": "Stáhnout",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Obnovit barvu",
|
"resetColor": "Obnovit barvu",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Gespeichert!",
|
"saved": "Gespeichert!",
|
||||||
"default": "Standard",
|
"default": "Standard",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"download": "Herunterladen"
|
"download": "Herunterladen",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Farbe zurücksetzen",
|
"resetColor": "Farbe zurücksetzen",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Gspeicheret!",
|
"saved": "Gspeicheret!",
|
||||||
"default": "Standard",
|
"default": "Standard",
|
||||||
"close": "Schlüüse",
|
"close": "Schlüüse",
|
||||||
"download": "Herunterladen"
|
"download": "Herunterladen",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Farb zruggsetze",
|
"resetColor": "Farb zruggsetze",
|
||||||
|
|
|
@ -16,6 +16,16 @@
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
},
|
},
|
||||||
|
"ready": {
|
||||||
|
"loading": "Vikunja is loading…",
|
||||||
|
"errorOccured": "An error occured:",
|
||||||
|
"checkApiUrl": "Please check if the api url is correct.",
|
||||||
|
"noApiUrlConfigured": "No API url was configured. Please set one below:"
|
||||||
|
},
|
||||||
|
"offline": {
|
||||||
|
"title": "You are offline.",
|
||||||
|
"text": "Please check your network connection and try again."
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
@ -353,6 +363,7 @@
|
||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"title": "Filters",
|
"title": "Filters",
|
||||||
|
"clear": "Clear Filters",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"titlePlaceholder": "The saved filter title goes here…",
|
"titlePlaceholder": "The saved filter title goes here…",
|
||||||
|
@ -654,6 +665,7 @@
|
||||||
"searchPlaceholder": "Type search for a new task to add as related…",
|
"searchPlaceholder": "Type search for a new task to add as related…",
|
||||||
"createPlaceholder": "Add this as new related task",
|
"createPlaceholder": "Add this as new related task",
|
||||||
"differentList": "This task belongs to a different list.",
|
"differentList": "This task belongs to a different list.",
|
||||||
|
"differentNamespace": "This task belongs to a different namespace.",
|
||||||
"noneYet": "No task relations yet.",
|
"noneYet": "No task relations yet.",
|
||||||
"delete": "Delete Task Relation",
|
"delete": "Delete Task Relation",
|
||||||
"deleteText1": "Are you sure you want to delete this task relation?",
|
"deleteText1": "Are you sure you want to delete this task relation?",
|
||||||
|
@ -754,10 +766,12 @@
|
||||||
},
|
},
|
||||||
"keyboardShortcuts": {
|
"keyboardShortcuts": {
|
||||||
"title": "Keyboard Shortcuts",
|
"title": "Keyboard Shortcuts",
|
||||||
|
"general": "General",
|
||||||
"allPages": "These shortcuts work on all pages.",
|
"allPages": "These shortcuts work on all pages.",
|
||||||
"currentPageOnly": "These shortcuts work only on the current page.",
|
"currentPageOnly": "These shortcuts work only on the current page.",
|
||||||
"toggleMenu": "Toggle The Menu",
|
"toggleMenu": "Toggle The Menu",
|
||||||
"quickSearch": "Open the search/quick action bar",
|
"quickSearch": "Open the search/quick action bar",
|
||||||
|
"then": "then",
|
||||||
"task": {
|
"task": {
|
||||||
"title": "Task Page",
|
"title": "Task Page",
|
||||||
"done": "Mark a task as done",
|
"done": "Mark a task as done",
|
||||||
|
@ -766,6 +780,13 @@
|
||||||
"dueDate": "Change the due date of this task",
|
"dueDate": "Change the due date of this task",
|
||||||
"attachment": "Add an attachment to this task",
|
"attachment": "Add an attachment to this task",
|
||||||
"related": "Modify related tasks of this task"
|
"related": "Modify related tasks of this task"
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"title": "List Views",
|
||||||
|
"switchToListView": "Switch to list view",
|
||||||
|
"switchToGanttView": "Switch to gantt view",
|
||||||
|
"switchToKanbanView": "Switch to kanban view",
|
||||||
|
"switchToTableView": "Switch to table view"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
|
@ -787,8 +808,9 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"signInOn": "Sign in to your Vikunja account on {0}",
|
"signInOn": "Sign in to your Vikunja account on {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\".",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\"."
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
"loadingError": {
|
"loadingError": {
|
||||||
"failed": "Loading failed, please {0}. If the error persists, please {1}.",
|
"failed": "Loading failed, please {0}. If the error persists, please {1}.",
|
||||||
|
@ -803,7 +825,7 @@
|
||||||
"quickActions": {
|
"quickActions": {
|
||||||
"commands": "Commands",
|
"commands": "Commands",
|
||||||
"placeholder": "Type a command or search…",
|
"placeholder": "Type a command or search…",
|
||||||
"hint": "You can use # to only search for tasks, * to only search for lists and @ to only search for teams.",
|
"hint": "You can use {list} to limit the search to a list. Combine {list} or {label} (labels) with a search query to search for a task with these labels or on that list. Use {assignee} to only search for teams.",
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"lists": "Lists",
|
"lists": "Lists",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Saved!",
|
"saved": "Saved!",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Reset Color",
|
"resetColor": "Reset Color",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Enregistré !",
|
"saved": "Enregistré !",
|
||||||
"default": "Par défaut",
|
"default": "Par défaut",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"download": "Télécharger"
|
"download": "Télécharger",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Réinitialiser la couleur",
|
"resetColor": "Réinitialiser la couleur",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Salvato!",
|
"saved": "Salvato!",
|
||||||
"default": "Predefinito",
|
"default": "Predefinito",
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
"download": "Scarica"
|
"download": "Scarica",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Ripristina Colore",
|
"resetColor": "Ripristina Colore",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Saved!",
|
"saved": "Saved!",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Reset Color",
|
"resetColor": "Reset Color",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Saved!",
|
"saved": "Saved!",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Reset Color",
|
"resetColor": "Reset Color",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Saved!",
|
"saved": "Saved!",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Reset Color",
|
"resetColor": "Reset Color",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Saved!",
|
"saved": "Saved!",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Reset Color",
|
"resetColor": "Reset Color",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Сохранено!",
|
"saved": "Сохранено!",
|
||||||
"default": "По умолчанию",
|
"default": "По умолчанию",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"download": "Скачать"
|
"download": "Скачать",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Сбросить цвет",
|
"resetColor": "Сбросить цвет",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Saved!",
|
"saved": "Saved!",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Reset Color",
|
"resetColor": "Reset Color",
|
||||||
|
|
|
@ -449,7 +449,9 @@
|
||||||
"saved": "Đã lưu!",
|
"saved": "Đã lưu!",
|
||||||
"default": "Mặc định",
|
"default": "Mặc định",
|
||||||
"close": "Đóng",
|
"close": "Đóng",
|
||||||
"download": "Tải về"
|
"download": "Tải về",
|
||||||
|
"showMenu": "Show the menu",
|
||||||
|
"hideMenu": "Hide the menu"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"resetColor": "Đặt lại màu",
|
"resetColor": "Đặt lại màu",
|
||||||
|
|
|
@ -31,8 +31,6 @@ import Notifications from '@kyvg/vue3-notification'
|
||||||
// PWA
|
// PWA
|
||||||
import './registerServiceWorker'
|
import './registerServiceWorker'
|
||||||
|
|
||||||
// Shortcuts
|
|
||||||
import shortkey from '@/plugins/shortkey'
|
|
||||||
// Vuex
|
// Vuex
|
||||||
import {store} from './store'
|
import {store} from './store'
|
||||||
// i18n
|
// i18n
|
||||||
|
@ -55,14 +53,14 @@ const app = createApp(App)
|
||||||
|
|
||||||
app.use(Notifications)
|
app.use(Notifications)
|
||||||
|
|
||||||
app.use(shortkey, {prevent: ['input', 'textarea', '.input', '[contenteditable]']})
|
|
||||||
|
|
||||||
// directives
|
// directives
|
||||||
import focus from './directives/focus'
|
import focus from './directives/focus'
|
||||||
import tooltip from './directives/tooltip'
|
import tooltip from './directives/tooltip'
|
||||||
|
import shortcut from '@/directives/shortcut'
|
||||||
|
|
||||||
app.directive('focus', focus)
|
app.directive('focus', focus)
|
||||||
app.directive('tooltip', tooltip)
|
app.directive('tooltip', tooltip)
|
||||||
|
app.directive('shortcut', shortcut)
|
||||||
|
|
||||||
// global components
|
// global components
|
||||||
import FontAwesomeIcon from './icons'
|
import FontAwesomeIcon from './icons'
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
function capitalizeFirstLetter(string) {
|
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const MODIFIER_KEYS = ['shift', 'ctrl', 'meta', 'alt']
|
|
||||||
|
|
||||||
const SHORT_CUT_INDEX = [
|
|
||||||
{ key: 'ArrowUp', value: 'arrowup' },
|
|
||||||
{ key: 'ArrowLeft', value: 'arrowlef' },
|
|
||||||
{ key: 'ArrowRight', value: 'arrowright' },
|
|
||||||
{ key: 'ArrowDown', value: 'arrowdown' },
|
|
||||||
{ key: 'AltGraph', value: 'altgraph' },
|
|
||||||
{ key: 'Escape', value: 'esc' },
|
|
||||||
{ key: 'Enter', value: 'enter' },
|
|
||||||
{ key: 'Tab', value: 'tab' },
|
|
||||||
{ key: ' ', value: 'space' },
|
|
||||||
{ key: 'PageUp', value: 'pagup' },
|
|
||||||
{ key: 'PageDown', value: 'pagedow' },
|
|
||||||
{ key: 'Home', value: 'home' },
|
|
||||||
{ key: 'End', value: 'end' },
|
|
||||||
{ key: 'Delete', value: 'del' },
|
|
||||||
{ key: 'Backspace', value: 'bacspace' },
|
|
||||||
{ key: 'Insert', value: 'insert' },
|
|
||||||
{ key: 'NumLock', value: 'numlock' },
|
|
||||||
{ key: 'CapsLock', value: 'capslock' },
|
|
||||||
{ key: 'Pause', value: 'pause' },
|
|
||||||
{ key: 'ContextMenu', value: 'cotextmenu' },
|
|
||||||
{ key: 'ScrollLock', value: 'scrolllock' },
|
|
||||||
{ key: 'BrowserHome', value: 'browserhome' },
|
|
||||||
{ key: 'MediaSelect', value: 'mediaselect' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export function encodeKey(pKey) {
|
|
||||||
const shortKey = {}
|
|
||||||
|
|
||||||
MODIFIER_KEYS.forEach((key) => {
|
|
||||||
shortKey[`${key}Key`] = pKey.includes(key)
|
|
||||||
})
|
|
||||||
|
|
||||||
let indexedKeys = createShortcutIndex(shortKey)
|
|
||||||
const vKey = pKey.filter(
|
|
||||||
(item) => !MODIFIER_KEYS.includes(item),
|
|
||||||
)
|
|
||||||
indexedKeys += vKey.join('')
|
|
||||||
return indexedKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
function createShortcutIndex(pKey) {
|
|
||||||
let k = ''
|
|
||||||
|
|
||||||
MODIFIER_KEYS.forEach((key) => {
|
|
||||||
if (pKey.key === capitalizeFirstLetter(key) || pKey[`${key}Key`]) {
|
|
||||||
k += key
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
SHORT_CUT_INDEX.forEach(({ key, value }) => {
|
|
||||||
if (pKey.key === key) {
|
|
||||||
k += value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (
|
|
||||||
(pKey.key && pKey.key !== ' ' && pKey.key.length === 1) ||
|
|
||||||
/F\d{1,2}|\//g.test(pKey.key)
|
|
||||||
) {
|
|
||||||
k += pKey.key.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
export { createShortcutIndex as decodeKey }
|
|
||||||
|
|
||||||
export function parseValue(value) {
|
|
||||||
value = typeof value === 'string' ? JSON.parse(value.replace(/'/gi, '"')) : value
|
|
||||||
|
|
||||||
return value instanceof Array ? { '': value } : value
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
import { parseValue, decodeKey, encodeKey } from './helpers'
|
|
||||||
|
|
||||||
let mapFunctions = {}
|
|
||||||
let objAvoided = []
|
|
||||||
let elementAvoided = []
|
|
||||||
let keyPressed = false
|
|
||||||
|
|
||||||
function dispatchShortkeyEvent(pKey) {
|
|
||||||
const e = new CustomEvent('shortkey', { bubbles: false })
|
|
||||||
|
|
||||||
if (mapFunctions[pKey].key) {
|
|
||||||
e.srcKey = mapFunctions[pKey].key
|
|
||||||
}
|
|
||||||
|
|
||||||
const elm = mapFunctions[pKey].el
|
|
||||||
|
|
||||||
if (!mapFunctions[pKey].propagte) {
|
|
||||||
elm[elm.length - 1].dispatchEvent(e)
|
|
||||||
} else {
|
|
||||||
elm.forEach((elmItem) => elmItem.dispatchEvent(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyDown(pKey) {
|
|
||||||
if (
|
|
||||||
(!mapFunctions[pKey].once && !mapFunctions[pKey].push) ||
|
|
||||||
(mapFunctions[pKey].push && !keyPressed)
|
|
||||||
) {
|
|
||||||
dispatchShortkeyEvent(pKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillMappingFunctions(
|
|
||||||
mappingFunctions,
|
|
||||||
{ b, push, once, focus, propagte, el },
|
|
||||||
) {
|
|
||||||
for (let key in b) {
|
|
||||||
const k = encodeKey(b[key])
|
|
||||||
const propagated = mappingFunctions[k] && mappingFunctions[k].propagte
|
|
||||||
const elm =
|
|
||||||
mappingFunctions[k] && mappingFunctions[k].el
|
|
||||||
? mappingFunctions[k].el
|
|
||||||
: []
|
|
||||||
|
|
||||||
elm.push(el)
|
|
||||||
|
|
||||||
mappingFunctions[k] = {
|
|
||||||
push,
|
|
||||||
once,
|
|
||||||
focus,
|
|
||||||
key,
|
|
||||||
propagte: propagated || propagte,
|
|
||||||
el: elm,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindValue(value, el, binding, vnode) {
|
|
||||||
const { modifiers } = binding
|
|
||||||
const push = !!modifiers.push
|
|
||||||
const avoid = !!modifiers.avoid
|
|
||||||
const focus = !modifiers.focus
|
|
||||||
const once = !!modifiers.once
|
|
||||||
const propagte = !!modifiers.propagte
|
|
||||||
|
|
||||||
if (avoid) {
|
|
||||||
objAvoided = objAvoided.filter((itm) => !itm === el)
|
|
||||||
objAvoided.push(el)
|
|
||||||
} else {
|
|
||||||
fillMappingFunctions(mapFunctions, {
|
|
||||||
b: value,
|
|
||||||
push,
|
|
||||||
once,
|
|
||||||
focus,
|
|
||||||
propagte,
|
|
||||||
el: vnode.el,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unbindValue(value, el) {
|
|
||||||
for (let key in value) {
|
|
||||||
const k = encodeKey(value[key])
|
|
||||||
const idxElm = mapFunctions[k].el.indexOf(el)
|
|
||||||
|
|
||||||
if (mapFunctions[k].el.length > 1 && idxElm > -1) {
|
|
||||||
mapFunctions[k].el.splice(idxElm, 1)
|
|
||||||
} else {
|
|
||||||
delete mapFunctions[k]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function availableElement(decodedKey) {
|
|
||||||
const objectIsAvoided = !!objAvoided.find(
|
|
||||||
(r) => r === document.activeElement,
|
|
||||||
)
|
|
||||||
const filterAvoided = !!elementAvoided.find(
|
|
||||||
(selector) =>
|
|
||||||
document.activeElement && document.activeElement.matches(selector),
|
|
||||||
)
|
|
||||||
return !!mapFunctions[decodedKey] && !(objectIsAvoided || filterAvoided)
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyDownListener(pKey) {
|
|
||||||
const decodedKey = decodeKey(pKey)
|
|
||||||
|
|
||||||
// Check avoidable elements
|
|
||||||
if (!availableElement(decodedKey)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mapFunctions[decodedKey].propagte) {
|
|
||||||
pKey.preventDefault()
|
|
||||||
pKey.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapFunctions[decodedKey].focus) {
|
|
||||||
keyDown(decodedKey)
|
|
||||||
keyPressed = true
|
|
||||||
} else if (!keyPressed) {
|
|
||||||
const elm = mapFunctions[decodedKey].el
|
|
||||||
elm[elm.length - 1].focus()
|
|
||||||
keyPressed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyUpListener(pKey) {
|
|
||||||
const decodedKey = decodeKey(pKey)
|
|
||||||
|
|
||||||
if (!availableElement(decodedKey)) {
|
|
||||||
keyPressed = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mapFunctions[decodedKey].propagte) {
|
|
||||||
pKey.preventDefault()
|
|
||||||
pKey.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapFunctions[decodedKey].once || mapFunctions[decodedKey].push) {
|
|
||||||
dispatchShortkeyEvent(decodedKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPressed = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// register key presses that happen before mounting of directive
|
|
||||||
// if (process?.env?.NODE_ENV !== 'test') {
|
|
||||||
// (() => {
|
|
||||||
document.addEventListener('keydown', keyDownListener, true)
|
|
||||||
document.addEventListener('keyup', keyUpListener, true)
|
|
||||||
// })()
|
|
||||||
// }
|
|
||||||
|
|
||||||
function install(app, options) {
|
|
||||||
elementAvoided = [...(options && options.prevent ? options.prevent : [])]
|
|
||||||
|
|
||||||
app.directive('shortkey', {
|
|
||||||
beforeMount(el, binding, vnode) {
|
|
||||||
// Mapping the commands
|
|
||||||
const value = parseValue(binding.value)
|
|
||||||
bindValue(value, el, binding, vnode)
|
|
||||||
},
|
|
||||||
|
|
||||||
updated(el, binding, vnode) {
|
|
||||||
const oldValue = parseValue(binding.oldValue)
|
|
||||||
unbindValue(oldValue, el)
|
|
||||||
|
|
||||||
const newValue = parseValue(binding.value)
|
|
||||||
bindValue(newValue, el, binding, vnode)
|
|
||||||
},
|
|
||||||
|
|
||||||
unmounted(el, binding) {
|
|
||||||
const value = parseValue(binding.value)
|
|
||||||
unbindValue(value, el)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
install,
|
|
||||||
encodeKey,
|
|
||||||
decodeKey,
|
|
||||||
keyDown,
|
|
||||||
}
|
|
|
@ -19,6 +19,7 @@ import attachments from './modules/attachments'
|
||||||
import labels from './modules/labels'
|
import labels from './modules/labels'
|
||||||
|
|
||||||
import ListService from '../services/list'
|
import ListService from '../services/list'
|
||||||
|
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||||
|
|
||||||
export const store = createStore({
|
export const store = createStore({
|
||||||
strict: import.meta.env.DEV,
|
strict: import.meta.env.DEV,
|
||||||
|
@ -43,6 +44,7 @@ export const store = createStore({
|
||||||
menuActive: true,
|
menuActive: true,
|
||||||
keyboardShortcutsActive: false,
|
keyboardShortcutsActive: false,
|
||||||
quickActionsActive: false,
|
quickActionsActive: false,
|
||||||
|
vikunjaReady: false,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
[LOADING](state, loading) {
|
[LOADING](state, loading) {
|
||||||
|
@ -84,6 +86,9 @@ export const store = createStore({
|
||||||
[BACKGROUND](state, background) {
|
[BACKGROUND](state, background) {
|
||||||
state.background = background
|
state.background = background
|
||||||
},
|
},
|
||||||
|
vikunjaReady(state, ready) {
|
||||||
|
state.vikunjaReady = ready
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async [CURRENT_LIST]({state, commit}, currentList) {
|
async [CURRENT_LIST]({state, commit}, currentList) {
|
||||||
|
@ -138,5 +143,10 @@ export const store = createStore({
|
||||||
|
|
||||||
commit(CURRENT_LIST, currentList)
|
commit(CURRENT_LIST, currentList)
|
||||||
},
|
},
|
||||||
|
async loadApp({commit, dispatch}) {
|
||||||
|
await checkAndSetApiUrl(window.API_URL)
|
||||||
|
await dispatch('auth/checkAuth')
|
||||||
|
commit('vikunjaReady', true)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import LabelService from '@/services/label'
|
import LabelService from '@/services/label'
|
||||||
import {setLoading} from '@/store/helper'
|
import {setLoading} from '@/store/helper'
|
||||||
import { success } from '@/message'
|
import {success} from '@/message'
|
||||||
import {i18n} from '@/i18n'
|
import {i18n} from '@/i18n'
|
||||||
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
|
import {getLabelsByIds, filterLabelsByQuery} from '@/helpers/labels'
|
||||||
|
|
||||||
|
@ -45,6 +45,11 @@ export default {
|
||||||
filterLabelsByQuery(state) {
|
filterLabelsByQuery(state) {
|
||||||
return (labelsToHide, query) => filterLabelsByQuery(state, labelsToHide, query)
|
return (labelsToHide, query) => filterLabelsByQuery(state, labelsToHide, query)
|
||||||
},
|
},
|
||||||
|
getLabelsByExactTitles(state) {
|
||||||
|
return labelTitles => Object
|
||||||
|
.values(state.labels)
|
||||||
|
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async loadAllLabels(ctx, {forceLoad} = {}) {
|
async loadAllLabels(ctx, {forceLoad} = {}) {
|
||||||
|
|
|
@ -76,8 +76,13 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getListAndNamespaceById: state => listId => {
|
getListAndNamespaceById: state => (listId, ignorePseudoNamespaces = false) => {
|
||||||
for (const n in state.namespaces) {
|
for (const n in state.namespaces) {
|
||||||
|
|
||||||
|
if(ignorePseudoNamespaces && state.namespaces[n].id < 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for (const l in state.namespaces[n].lists) {
|
for (const l in state.namespaces[n].lists) {
|
||||||
if (state.namespaces[n].lists[l].id === listId) {
|
if (state.namespaces[n].lists[l].id === listId) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -34,7 +34,6 @@ $filter-container-top-link-share-list: -47px;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-top: calc(1rem - 1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fancycheckbox {
|
.fancycheckbox {
|
||||||
|
@ -47,10 +46,6 @@ $filter-container-top-link-share-list: -47px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-right: .5rem;
|
margin-right: .5rem;
|
||||||
|
|
||||||
.button, .input {
|
|
||||||
height: $switch-view-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
transition: width $transition;
|
transition: width $transition;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,30 +1,38 @@
|
||||||
// FIXME: move to loading.vue
|
// FIXME: move to loading.vue
|
||||||
.loader-container.is-loading {
|
.loader-container.is-loading {
|
||||||
position: relative;
|
position: relative;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@include loader;
|
@include loader;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(50% - 2.5rem);
|
top: calc(50% - 2.5rem);
|
||||||
left: calc(50% - 2.5rem);
|
left: calc(50% - 2.5rem);
|
||||||
width: 5rem;
|
width: 5rem;
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
border-width: 0.25rem;
|
border-width: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-loading-small::after {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
top: calc(50% - .75rem);
|
||||||
|
left: calc(50% - .75rem);
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: move to ShowTasks.vue
|
// FIXME: move to ShowTasks.vue
|
||||||
.spinner.is-loading {
|
.spinner.is-loading {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@include loader;
|
@include loader;
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
margin-left: calc(50% - 1rem);
|
margin-left: calc(50% - 1rem);
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
border-width: 0.25rem;
|
border-width: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,21 +6,29 @@
|
||||||
<div class="switch-view-container">
|
<div class="switch-view-container">
|
||||||
<div class="switch-view">
|
<div class="switch-view">
|
||||||
<router-link
|
<router-link
|
||||||
|
v-shortcut="'g l'"
|
||||||
|
:title="$t('keyboardShortcuts.list.switchToListView')"
|
||||||
:class="{'is-active': $route.name.includes('list.list')}"
|
:class="{'is-active': $route.name.includes('list.list')}"
|
||||||
:to="{ name: 'list.list', params: { listId: listId } }">
|
:to="{ name: 'list.list', params: { listId: listId } }">
|
||||||
{{ $t('list.list.title') }}
|
{{ $t('list.list.title') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-shortcut="'g g'"
|
||||||
|
:title="$t('keyboardShortcuts.list.switchToGanttView')"
|
||||||
:class="{'is-active': $route.name.includes('list.gantt')}"
|
:class="{'is-active': $route.name.includes('list.gantt')}"
|
||||||
:to="{ name: 'list.gantt', params: { listId: listId } }">
|
:to="{ name: 'list.gantt', params: { listId: listId } }">
|
||||||
{{ $t('list.gantt.title') }}
|
{{ $t('list.gantt.title') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-shortcut="'g t'"
|
||||||
|
:title="$t('keyboardShortcuts.list.switchToTableView')"
|
||||||
:class="{'is-active': $route.name.includes('list.table')}"
|
:class="{'is-active': $route.name.includes('list.table')}"
|
||||||
:to="{ name: 'list.table', params: { listId: listId } }">
|
:to="{ name: 'list.table', params: { listId: listId } }">
|
||||||
{{ $t('list.table.title') }}
|
{{ $t('list.table.title') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
|
v-shortcut="'g k'"
|
||||||
|
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
|
||||||
:class="{'is-active': $route.name.includes('list.kanban')}"
|
:class="{'is-active': $route.name.includes('list.kanban')}"
|
||||||
:to="{ name: 'list.kanban', params: { listId: listId } }">
|
:to="{ name: 'list.kanban', params: { listId: listId } }">
|
||||||
{{ $t('list.kanban.title') }}
|
{{ $t('list.kanban.title') }}
|
||||||
|
|
|
@ -2,18 +2,11 @@
|
||||||
<div class="kanban-view">
|
<div class="kanban-view">
|
||||||
<div class="filter-container" v-if="isSavedFilter">
|
<div class="filter-container" v-if="isSavedFilter">
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<x-button
|
<filter-popup
|
||||||
@click.prevent.stop="toggleFilterPopup"
|
v-model="params"
|
||||||
icon="filter"
|
@update:modelValue="loadBuckets"
|
||||||
type="secondary"
|
/>
|
||||||
>
|
|
||||||
{{ $t('filters.title') }}
|
|
||||||
</x-button>
|
|
||||||
</div>
|
</div>
|
||||||
<filter-popup
|
|
||||||
:visible="showFilters"
|
|
||||||
v-model="params"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="{ 'is-loading': loading && !oneTaskUpdating}"
|
:class="{ 'is-loading': loading && !oneTaskUpdating}"
|
||||||
|
@ -143,7 +136,7 @@
|
||||||
:component-data="taskDraggableTaskComponentData"
|
:component-data="taskDraggableTaskComponentData"
|
||||||
>
|
>
|
||||||
<template #item="{element: task}">
|
<template #item="{element: task}">
|
||||||
<kanban-card :task="task" />
|
<kanban-card :task="task"/>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
|
@ -213,7 +206,7 @@
|
||||||
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
<component :is="Component" />
|
<component :is="Component"/>
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|
||||||
|
@ -224,10 +217,10 @@
|
||||||
v-if="showBucketDeleteModal"
|
v-if="showBucketDeleteModal"
|
||||||
>
|
>
|
||||||
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
|
<template #header><span>{{ $t('list.kanban.deleteHeaderBucket') }}</span></template>
|
||||||
|
|
||||||
<template #text>
|
<template #text>
|
||||||
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
|
<p>{{ $t('list.kanban.deleteBucketText1') }}<br/>
|
||||||
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
{{ $t('list.kanban.deleteBucketText2') }}</p>
|
||||||
</template>
|
</template>
|
||||||
</modal>
|
</modal>
|
||||||
</transition>
|
</transition>
|
||||||
|
@ -300,7 +293,6 @@ export default {
|
||||||
filter_comparator: [],
|
filter_comparator: [],
|
||||||
filter_concat: 'and',
|
filter_concat: 'and',
|
||||||
},
|
},
|
||||||
showFilters: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -328,10 +320,10 @@ export default {
|
||||||
return {
|
return {
|
||||||
type: 'transition',
|
type: 'transition',
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
name: !this.dragBucket ? 'move-bucket': null,
|
name: !this.dragBucket ? 'move-bucket' : null,
|
||||||
class: [
|
class: [
|
||||||
'kanban-bucket-container',
|
'kanban-bucket-container',
|
||||||
{ 'dragging-disabled': !this.canWrite },
|
{'dragging-disabled': !this.canWrite},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -339,10 +331,10 @@ export default {
|
||||||
return {
|
return {
|
||||||
type: 'transition',
|
type: 'transition',
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
name: !this.drag ? 'move-card': null,
|
name: !this.drag ? 'move-card' : null,
|
||||||
class: [
|
class: [
|
||||||
'dropper',
|
'dropper',
|
||||||
{ 'dragging-disabled': !this.canWrite },
|
{'dragging-disabled': !this.canWrite},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -357,19 +349,15 @@ export default {
|
||||||
list: state => state.currentList,
|
list: state => state.currentList,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
|
||||||
toggleFilterPopup() {
|
|
||||||
this.showFilters = !this.showFilters
|
|
||||||
},
|
|
||||||
|
|
||||||
|
methods: {
|
||||||
loadBuckets() {
|
loadBuckets() {
|
||||||
// Prevent trying to load buckets if the task popup view is active
|
// Prevent trying to load buckets if the task popup view is active
|
||||||
if (this.$route.name !== 'list.kanban') {
|
if (this.$route.name !== 'list.kanban') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { listId, params } = this.loadBucketParameter
|
const {listId, params} = this.loadBucketParameter
|
||||||
|
|
||||||
this.collapsedBuckets = getCollapsedBucketState(listId)
|
this.collapsedBuckets = getCollapsedBucketState(listId)
|
||||||
|
|
||||||
|
@ -424,7 +412,7 @@ export default {
|
||||||
|
|
||||||
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations
|
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations
|
||||||
newTask.bucketId = newBucket.id,
|
newTask.bucketId = newBucket.id,
|
||||||
newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null)
|
newTask.kanbanPosition = calculateItemPosition(taskBefore !== null ? taskBefore.kanbanPosition : null, taskAfter !== null ? taskAfter.kanbanPosition : null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('tasks/update', newTask)
|
await this.$store.dispatch('tasks/update', newTask)
|
||||||
|
|
|
@ -41,19 +41,11 @@
|
||||||
v-if="!showTaskSearch"
|
v-if="!showTaskSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<x-button
|
<filter-popup
|
||||||
@click.prevent.stop="showTaskFilter = !showTaskFilter"
|
v-model="params"
|
||||||
type="secondary"
|
@update:modelValue="loadTasks()"
|
||||||
icon="filter"
|
/>
|
||||||
>
|
|
||||||
{{ $t('filters.title') }}
|
|
||||||
</x-button>
|
|
||||||
</div>
|
</div>
|
||||||
<filter-popup
|
|
||||||
:visible="showTaskFilter"
|
|
||||||
v-model="params"
|
|
||||||
@update:modelValue="loadTasks()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<card :padding="false" :has-content="false" class="has-overflow">
|
<card :padding="false" :has-content="false" class="has-overflow">
|
||||||
|
@ -126,7 +118,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
:total-pages="taskCollectionService.totalPages"
|
:total-pages="taskCollectionService.totalPages"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
/>
|
/>
|
||||||
|
@ -135,7 +127,7 @@
|
||||||
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
<component :is="Component" />
|
<component :is="Component"/>
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,6 +147,7 @@ import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||||
import {HAS_TASKS} from '@/store/mutation-types'
|
import {HAS_TASKS} from '@/store/mutation-types'
|
||||||
import Nothing from '@/components/misc/nothing.vue'
|
import Nothing from '@/components/misc/nothing.vue'
|
||||||
import Pagination from '@/components/misc/pagination.vue'
|
import Pagination from '@/components/misc/pagination.vue'
|
||||||
|
import Popup from '@/components/misc/popup'
|
||||||
|
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
import {calculateItemPosition} from '../../../helpers/calculateItemPosition'
|
||||||
|
@ -198,6 +191,7 @@ export default {
|
||||||
taskList,
|
taskList,
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
|
Popup,
|
||||||
Nothing,
|
Nothing,
|
||||||
FilterPopup,
|
FilterPopup,
|
||||||
SingleTaskInList,
|
SingleTaskInList,
|
||||||
|
@ -294,11 +288,11 @@ export default {
|
||||||
|
|
||||||
async saveTaskPosition(e) {
|
async saveTaskPosition(e) {
|
||||||
this.drag = false
|
this.drag = false
|
||||||
|
|
||||||
const task = this.tasks[e.newIndex]
|
const task = this.tasks[e.newIndex]
|
||||||
const taskBefore = this.tasks[e.newIndex - 1] ?? null
|
const taskBefore = this.tasks[e.newIndex - 1] ?? null
|
||||||
const taskAfter = this.tasks[e.newIndex + 1] ?? null
|
const taskAfter = this.tasks[e.newIndex + 1] ?? null
|
||||||
|
|
||||||
const newTask = {
|
const newTask = {
|
||||||
...task,
|
...task,
|
||||||
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
|
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
|
||||||
|
|
|
@ -2,67 +2,63 @@
|
||||||
<div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container">
|
<div :class="{'is-loading': taskCollectionService.loading}" class="table-view loader-container">
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<x-button
|
<popup>
|
||||||
@click.prevent.stop="() => {showActiveColumnsFilter = !showActiveColumnsFilter; showTaskFilter = false}"
|
<template #trigger="{toggle}">
|
||||||
icon="th"
|
<x-button
|
||||||
type="secondary"
|
@click.prevent.stop="toggle()"
|
||||||
>
|
icon="th"
|
||||||
{{ $t('list.table.columns') }}
|
type="secondary"
|
||||||
</x-button>
|
>
|
||||||
<x-button
|
{{ $t('list.table.columns') }}
|
||||||
@click.prevent.stop="() => {showTaskFilter = !showTaskFilter; showActiveColumnsFilter = false}"
|
</x-button>
|
||||||
icon="filter"
|
</template>
|
||||||
type="secondary"
|
<template #content="{isOpen}">
|
||||||
>
|
<card class="columns-filter" :class="{'is-open': isOpen}">
|
||||||
{{ $t('filters.title') }}
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
|
||||||
</x-button>
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">
|
||||||
|
{{ $t('task.attributes.done') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">
|
||||||
|
{{ $t('task.attributes.title') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">
|
||||||
|
{{ $t('task.attributes.priority') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">
|
||||||
|
{{ $t('task.attributes.labels') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">
|
||||||
|
{{ $t('task.attributes.assignees') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">
|
||||||
|
{{ $t('task.attributes.dueDate') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">
|
||||||
|
{{ $t('task.attributes.startDate') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">
|
||||||
|
{{ $t('task.attributes.endDate') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">
|
||||||
|
{{ $t('task.attributes.percentDone') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">
|
||||||
|
{{ $t('task.attributes.created') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">
|
||||||
|
{{ $t('task.attributes.updated') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">
|
||||||
|
{{ $t('task.attributes.createdBy') }}
|
||||||
|
</fancycheckbox>
|
||||||
|
</card>
|
||||||
|
</template>
|
||||||
|
</popup>
|
||||||
|
<filter-popup
|
||||||
|
v-model="params"
|
||||||
|
@update:modelValue="loadTasks()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<transition name="fade">
|
|
||||||
<card v-if="showActiveColumnsFilter">
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.id">#</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.done">
|
|
||||||
{{ $t('task.attributes.done') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.title">
|
|
||||||
{{ $t('task.attributes.title') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.priority">
|
|
||||||
{{ $t('task.attributes.priority') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.labels">
|
|
||||||
{{ $t('task.attributes.labels') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.assignees">
|
|
||||||
{{ $t('task.attributes.assignees') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.dueDate">
|
|
||||||
{{ $t('task.attributes.dueDate') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.startDate">
|
|
||||||
{{ $t('task.attributes.startDate') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.endDate">
|
|
||||||
{{ $t('task.attributes.endDate') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.percentDone">
|
|
||||||
{{ $t('task.attributes.percentDone') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.created">
|
|
||||||
{{ $t('task.attributes.created') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.updated">
|
|
||||||
{{ $t('task.attributes.updated') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
<fancycheckbox @change="saveTaskColumns" v-model="activeColumns.createdBy">
|
|
||||||
{{ $t('task.attributes.createdBy') }}
|
|
||||||
</fancycheckbox>
|
|
||||||
</card>
|
|
||||||
</transition>
|
|
||||||
<filter-popup
|
|
||||||
:visible="showTaskFilter"
|
|
||||||
v-model="params"
|
|
||||||
@update:modelValue="loadTasks()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<card :padding="false" :has-content="false">
|
<card :padding="false" :has-content="false">
|
||||||
|
@ -189,21 +185,23 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import taskList from '../../../components/tasks/mixins/taskList'
|
import taskList from '@/components/tasks/mixins/taskList'
|
||||||
import Done from '@/components/misc/Done.vue'
|
import Done from '@/components/misc/Done.vue'
|
||||||
import User from '../../../components/misc/user'
|
import User from '@/components/misc/user'
|
||||||
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
|
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
|
||||||
import Labels from '../../../components/tasks/partials/labels'
|
import Labels from '@/components/tasks/partials/labels'
|
||||||
import DateTableCell from '../../../components/tasks/partials/date-table-cell'
|
import DateTableCell from '@/components/tasks/partials/date-table-cell'
|
||||||
import Fancycheckbox from '../../../components/input/fancycheckbox'
|
import Fancycheckbox from '@/components/input/fancycheckbox'
|
||||||
import Sort from '../../../components/tasks/partials/sort'
|
import Sort from '@/components/tasks/partials/sort'
|
||||||
import {saveListView} from '@/helpers/saveListView'
|
import {saveListView} from '@/helpers/saveListView'
|
||||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||||
import Pagination from '@/components/misc/pagination.vue'
|
import Pagination from '@/components/misc/pagination.vue'
|
||||||
|
import Popup from '@/components/misc/popup'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Table',
|
name: 'Table',
|
||||||
components: {
|
components: {
|
||||||
|
Popup,
|
||||||
Done,
|
Done,
|
||||||
FilterPopup,
|
FilterPopup,
|
||||||
Sort,
|
Sort,
|
||||||
|
@ -219,7 +217,6 @@ export default {
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showActiveColumnsFilter: false,
|
|
||||||
activeColumns: {
|
activeColumns: {
|
||||||
id: true,
|
id: true,
|
||||||
done: true,
|
done: true,
|
||||||
|
@ -323,4 +320,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.columns-filter {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
margin: 2rem 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -270,18 +270,16 @@
|
||||||
/>
|
/>
|
||||||
<x-button
|
<x-button
|
||||||
@click="setFieldActive('assignees')"
|
@click="setFieldActive('assignees')"
|
||||||
@shortkey="setFieldActive('assignees')"
|
|
||||||
type="secondary"
|
type="secondary"
|
||||||
v-shortkey="['a']">
|
v-shortcut="'a'">
|
||||||
<span class="icon is-small"><icon icon="users"/></span>
|
<span class="icon is-small"><icon icon="users"/></span>
|
||||||
{{ $t('task.detail.actions.assign') }}
|
{{ $t('task.detail.actions.assign') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
@click="setFieldActive('labels')"
|
@click="setFieldActive('labels')"
|
||||||
@shortkey="setFieldActive('labels')"
|
|
||||||
type="secondary"
|
type="secondary"
|
||||||
v-shortkey="['l']"
|
|
||||||
icon="tags"
|
icon="tags"
|
||||||
|
v-shortcut="'l'"
|
||||||
>
|
>
|
||||||
{{ $t('task.detail.actions.label') }}
|
{{ $t('task.detail.actions.label') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
|
@ -294,10 +292,9 @@
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
@click="setFieldActive('dueDate')"
|
@click="setFieldActive('dueDate')"
|
||||||
@shortkey="setFieldActive('dueDate')"
|
|
||||||
type="secondary"
|
type="secondary"
|
||||||
v-shortkey="['d']"
|
|
||||||
icon="calendar"
|
icon="calendar"
|
||||||
|
v-shortcut="'d'"
|
||||||
>
|
>
|
||||||
{{ $t('task.detail.actions.dueDate') }}
|
{{ $t('task.detail.actions.dueDate') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
|
@ -338,19 +335,17 @@
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
@click="setFieldActive('attachments')"
|
@click="setFieldActive('attachments')"
|
||||||
@shortkey="setFieldActive('attachments')"
|
|
||||||
type="secondary"
|
type="secondary"
|
||||||
v-shortkey="['f']"
|
|
||||||
icon="paperclip"
|
icon="paperclip"
|
||||||
|
v-shortcut="'f'"
|
||||||
>
|
>
|
||||||
{{ $t('task.detail.actions.attachments') }}
|
{{ $t('task.detail.actions.attachments') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
@click="setFieldActive('relatedTasks')"
|
@click="setFieldActive('relatedTasks')"
|
||||||
@shortkey="setFieldActive('relatedTasks')"
|
|
||||||
type="secondary"
|
type="secondary"
|
||||||
v-shortkey="['r']"
|
|
||||||
icon="sitemap"
|
icon="sitemap"
|
||||||
|
v-shortcut="'r'"
|
||||||
>
|
>
|
||||||
{{ $t('task.detail.actions.relatedTasks') }}
|
{{ $t('task.detail.actions.relatedTasks') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
|
|
|
@ -1088,6 +1088,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-5.tgz#6251e6917198362fa56510eb256cfb6aa6d30a32"
|
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-5.tgz#6251e6917198362fa56510eb256cfb6aa6d30a32"
|
||||||
integrity sha512-aNmBT4bOecrFsZTog1l6AJDQHPP3ocXV+WQ3Ogy8WZCqstB/ahfhH4CPu5i4N9Hw0MBKXqE+LX+NbUxcj8cVTw==
|
integrity sha512-aNmBT4bOecrFsZTog1l6AJDQHPP3ocXV+WQ3Ogy8WZCqstB/ahfhH4CPu5i4N9Hw0MBKXqE+LX+NbUxcj8cVTw==
|
||||||
|
|
||||||
|
"@github/hotkey@^1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@github/hotkey/-/hotkey-1.6.0.tgz#64da82a18ac11d24f9d5d61575a0a58ba101b2ab"
|
||||||
|
integrity sha512-pm/xBWrn0yyD2GFqPUBH4ne7mdpdrnmdHxwKV0hN/jnSKj01RTPxau65SAvBvWD1Pf2VRv/OEE4H9ECORBHGdg==
|
||||||
|
|
||||||
"@hapi/hoek@^9.0.0":
|
"@hapi/hoek@^9.0.0":
|
||||||
version "9.2.1"
|
version "9.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17"
|
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17"
|
||||||
|
@ -12084,7 +12089,7 @@ vite-plugin-pwa@0.11.3:
|
||||||
workbox-build "^6.3.0"
|
workbox-build "^6.3.0"
|
||||||
workbox-window "^6.3.0"
|
workbox-window "^6.3.0"
|
||||||
|
|
||||||
vite-svg-loader@^3.1.0:
|
vite-svg-loader@3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/vite-svg-loader/-/vite-svg-loader-3.1.0.tgz#58d4ed5785583afce10059125467f8bf803e3183"
|
resolved "https://registry.yarnpkg.com/vite-svg-loader/-/vite-svg-loader-3.1.0.tgz#58d4ed5785583afce10059125467f8bf803e3183"
|
||||||
integrity sha512-dcxLJPQXC3e2/HYrKjOi0fEIzP+Wcny1PhOTsYhnZcqLIhyjAHkMeZwWl9jXC3u+QpUZy4TyLgVnBPuiok3BUA==
|
integrity sha512-dcxLJPQXC3e2/HYrKjOi0fEIzP+Wcny1PhOTsYhnZcqLIhyjAHkMeZwWl9jXC3u+QpUZy4TyLgVnBPuiok3BUA==
|
||||||
|
|
Reference in New Issue
Block a user