This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/App.vue

503 lines
16 KiB
Vue
Raw Normal View History

2018-08-28 20:50:22 +00:00
<template>
2019-11-03 12:44:40 +00:00
<div>
<div v-if="online">
2019-11-03 12:44:40 +00:00
<!-- 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>
<nav
class="navbar main-theme is-fixed-top"
:class="{'has-background': background}"
role="navigation"
aria-label="main navigation"
v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)">
2019-11-03 12:44:40 +00:00
<div class="navbar-brand">
<router-link :to="{name: 'home'}" class="navbar-item logo">
2020-06-14 20:43:08 +00:00
<img src="/images/logo-full-pride.svg" alt="Vikunja" v-if="(new Date()).getMonth() === 5"/>
<img src="/images/logo-full.svg" alt="Vikunja" v-else/>
2019-11-03 12:44:40 +00:00
</router-link>
2020-06-25 21:56:41 +00:00
<a
@click="menuActive = true"
class="menu-show-button"
:class="{'is-visible': !menuActive}"
>
<icon icon="bars"></icon>
</a>
2019-11-03 12:44:40 +00:00
</div>
2020-06-25 21:56:41 +00:00
<a
@click="menuActive = true"
class="menu-show-button"
>
2020-06-12 17:32:37 +00:00
<icon icon="bars"></icon>
</a>
<div class="list-title" v-if="currentList.id">
<h1
class="title"
:style="{ 'opacity': currentList.title === '' ? '0': '1' }">
{{ currentList.title === '' ? 'Loading...': currentList.title}}
</h1>
<router-link
:to="{ name: 'list.edit', params: { id: currentList.id } }"
class="icon"
v-if="canWriteCurrentList">
<icon icon="cog" size="2x"/>
</router-link>
</div>
2019-11-03 12:44:40 +00:00
<div class="navbar-end">
<div v-if="updateAvailable" class="update-notification">
<p>There is an update for Vikunja available!</p>
<a @click="refreshApp()" class="button is-primary noshadow">Update Now</a>
</div>
2019-11-03 12:44:40 +00:00
<div class="user">
<img :src="userAvatar" class="avatar" alt=""/>
2019-11-03 12:44:40 +00:00
<div class="dropdown is-right is-active">
<div class="dropdown-trigger">
<button class="button noshadow" @click.stop="userMenuActive = !userMenuActive">
<span class="username">{{userInfo.username}}</span>
2019-11-03 12:44:40 +00:00
<span class="icon is-small">
2019-04-23 19:50:37 +00:00
<icon icon="chevron-down"/>
</span>
2019-11-03 12:44:40 +00:00
</button>
</div>
<transition name="fade">
<div class="dropdown-menu" v-if="userMenuActive">
<div class="dropdown-content">
<router-link :to="{name: 'user.settings'}" class="dropdown-item">
Settings
</router-link>
<a :href="imprintUrl" v-if="imprintUrl" class="dropdown-item" target="_blank">Imprint</a>
<a :href="privacyPolicyUrl" v-if="privacyPolicyUrl" class="dropdown-item" target="_blank">Privacy policy</a>
<a @click="keyboardShortcutsActive = true" class="dropdown-item">Keyboard Shortcuts</a>
2019-11-03 12:44:40 +00:00
<a @click="logout()" class="dropdown-item">
Logout
</a>
</div>
</div>
</transition>
</div>
</div>
</div>
</nav>
<div v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)">
2020-06-25 21:56:41 +00:00
<a @click="menuActive = false" class="menu-hide-button" v-if="menuActive">
2019-11-03 12:44:40 +00:00
<icon icon="times"></icon>
</a>
<div
class="app-container"
:class="{'has-background': background}"
:style="{'background-image': `url(${background})`}"
>
2020-06-25 21:56:41 +00:00
<div class="namespace-container" :class="{'is-active': menuActive}">
2019-11-03 12:44:40 +00:00
<div class="menu top-menu">
<router-link :to="{name: 'home'}" class="logo">
<img src="/images/logo-full.svg" alt="Vikunja"/>
</router-link>
2019-11-03 12:44:40 +00:00
<ul class="menu-list">
<li>
<router-link :to="{ name: 'home'}">
2018-12-25 15:03:51 +00:00
<span class="icon">
<icon icon="calendar"/>
</span>
2019-11-03 12:44:40 +00:00
Overview
</router-link>
</li>
<li>
<router-link :to="{ name: 'tasks.range', params: {type: 'week'}}">
<span class="icon">
<icon icon="calendar-week"/>
</span>
Next Week
2019-11-03 12:44:40 +00:00
</router-link>
</li>
<li>
<router-link :to="{ name: 'tasks.range', params: {type: 'month'}}">
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
Next Month
2019-11-03 12:44:40 +00:00
</router-link>
</li>
<li>
<router-link :to="{ name: 'teams.index'}">
2018-12-25 15:03:51 +00:00
<span class="icon">
<icon icon="users"/>
</span>
2019-11-03 12:44:40 +00:00
Teams
</router-link>
</li>
<li>
<router-link :to="{ name: 'namespaces.index'}">
2018-12-25 15:03:51 +00:00
<span class="icon">
<icon icon="layer-group"/>
</span>
Namespaces & Lists
2019-11-03 12:44:40 +00:00
</router-link>
</li>
<li>
<router-link :to="{ name: 'labels.index'}">
2019-03-07 19:48:40 +00:00
<span class="icon">
<icon icon="tags"/>
</span>
2019-11-03 12:44:40 +00:00
Labels
</router-link>
</li>
</ul>
</div>
<a @click="menuActive = false" class="collapse-menu-button" v-shortkey="['ctrl', 'e']" @shortkey="() => menuActive = !menuActive">
Collapse Menu
</a>
2019-11-03 12:44:40 +00:00
<aside class="menu namespaces-lists">
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
<template v-for="n in namespaces">
<div :key="n.id">
<router-link
v-tooltip.right="'Settings'"
:to="{name: 'namespace.edit', params: {id: n.id} }"
class="nsettings"
2019-11-03 12:44:40 +00:00
v-if="n.id > 0">
2018-12-25 15:03:51 +00:00
<span class="icon">
<icon icon="cog"/>
</span>
2019-11-03 12:44:40 +00:00
</router-link>
<router-link
v-tooltip="'Add a new list in the ' + n.title + ' namespace'"
:to="{ name: 'list.create', params: { id: n.id} }"
class="nsettings"
:key="n.id + 'list.create'"
v-if="n.id > 0">
2018-12-25 15:03:51 +00:00
<span class="icon">
<icon icon="plus"/>
</span>
2019-11-03 12:44:40 +00:00
</router-link>
<label
class="menu-label"
v-tooltip="n.title + ' (' + n.lists.length + ')'"
:for="n.id + 'checker'">
<span class="name">
<span
class="color-bubble"
v-if="n.hexColor !== ''"
:style="{ backgroundColor: n.hexColor }">
</span>
{{n.title}} ({{n.lists.length}})
</span>
2019-11-03 12:44:40 +00:00
</label>
</div>
<input
:key="n.id + 'checker'"
type="checkbox"
checked="checked"
:id="n.id + 'checker'"
class="checkinput"/>
2019-11-03 12:44:40 +00:00
<div class="more-container" :key="n.id + 'child'">
<ul class="menu-list can-be-hidden">
<template v-for="l in n.lists">
<!-- This is a bit ugly but vue wouldn't want to let me filter this - probably because the lists
are nested inside of the namespaces makes it a lot harder.-->
<li v-if="!l.isArchived" :key="l.id">
<router-link
:to="{ name: 'list.index', params: { listId: l.id} }"
:class="{'router-link-exact-active': currentList.id === l.id}">
<span class="name">
<span
class="color-bubble"
v-if="l.hexColor !== ''"
:style="{ backgroundColor: l.hexColor }">
</span>
{{l.title}}
</span>
</router-link>
</li>
</template>
2019-11-03 12:44:40 +00:00
</ul>
<label class="hidden-hint" :for="n.id + 'checker'">
Show hidden lists ({{n.lists.length}})...
</label>
</div>
</template>
</aside>
<a class="menu-bottom-link" target="_blank" href="https://vikunja.io">Powered by Vikunja</a>
2019-11-03 12:44:40 +00:00
</div>
2020-06-25 21:56:41 +00:00
<div
class="app-content"
2020-08-11 18:47:27 +00:00
:class="[
{
'fullpage-overlay': fullpage,
'is-menu-enabled': menuActive,
},
$route.name,
]"
2020-06-25 21:56:41 +00:00
>
<a class="mobile-overlay" v-if="menuActive" @click="menuActive = false"></a>
2019-11-03 12:44:40 +00:00
<transition name="fade">
<router-view/>
</transition>
<a class="keyboard-shortcuts-button" @click="keyboardShortcutsActive = true">
<icon icon="keyboard"/>
</a>
2019-11-03 12:44:40 +00:00
</div>
</div>
</div>
<div
v-else-if="userAuthenticated && (userInfo && userInfo.type === authTypes.LINK_SHARE)"
class="link-share-container"
:class="{'has-background': background}"
:style="{'background-image': `url(${background})`}"
>
2019-11-03 12:44:40 +00:00
<div class="container has-text-centered link-share-view">
<div class="column is-10 is-offset-1">
<img src="/images/logo-full.svg" alt="Vikunja" class="logo"/>
<h1
class="title"
:style="{ 'opacity': currentList.title === '' ? '0': '1' }">
{{ currentList.title === '' ? 'Loading...': currentList.title}}
</h1>
<div class="box has-text-left view">
2019-11-03 12:44:40 +00:00
<div class="logout">
<a @click="logout()" class="button">
2019-11-03 12:44:40 +00:00
<span>Logout</span>
<span class="icon is-small">
2019-09-09 17:55:43 +00:00
<icon icon="sign-out-alt"/>
</span>
2019-11-03 12:44:40 +00:00
</a>
</div>
<router-view/>
</div>
</div>
</div>
</div>
<div v-else>
<div class="noauth-container">
<img src="/images/logo-full.svg" alt="Vikunja"/>
<div class="message is-info" v-if="motd !== ''">
<div class="message-header">
<p>Info</p>
</div>
<div class="message-body">
{{ motd }}
2019-12-25 16:38:49 +00:00
</div>
2019-11-03 12:44:40 +00:00
</div>
<router-view/>
2019-11-03 12:44:40 +00:00
</div>
</div>
<notification/>
2019-11-03 12:44:40 +00:00
</div>
<div class="app offline" v-else>
<div class="offline-message">
<h1>You are offline.</h1>
<p>Please check your network connection and try again.</p>
</div>
</div>
<transition name="fade">
<keyboard-shortcuts v-if="keyboardShortcutsActive" @close="keyboardShortcutsActive = false"/>
</transition>
2019-11-03 12:44:40 +00:00
</div>
2018-08-28 20:50:22 +00:00
</template>
<script>
import router from './router'
import {mapState} from 'vuex'
2019-09-09 17:55:43 +00:00
import NamespaceService from './services/namespace'
2019-09-09 17:55:43 +00:00
import authTypes from './models/authTypes'
import Rights from './models/rights.json'
2018-09-07 06:42:17 +00:00
import swEvents from './ServiceWorker/events'
2020-06-17 20:15:59 +00:00
import Notification from './components/misc/notification'
import {CURRENT_LIST, IS_FULLPAGE, ONLINE} from './store/mutation-types'
import KeyboardShortcuts from './components/misc/keyboard-shortcuts'
export default {
name: 'app',
components: {
KeyboardShortcuts,
Notification,
},
data() {
return {
namespaceService: NamespaceService,
2020-06-25 21:56:41 +00:00
menuActive: true,
currentDate: new Date(),
2019-04-23 19:50:37 +00:00
userMenuActive: false,
2019-09-09 17:55:43 +00:00
authTypes: authTypes,
keyboardShortcutsActive: false,
// Service Worker stuff
updateAvailable: false,
registration: null,
refreshing: false,
}
},
2018-11-01 21:34:29 +00:00
beforeMount() {
// Check if the user is offline, show a message then
this.$store.commit(ONLINE, navigator.onLine)
window.addEventListener('online', () => this.$store.commit(ONLINE, navigator.onLine));
window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine));
// Password reset
if (this.$route.query.userPasswordReset !== undefined) {
2018-11-01 21:34:29 +00:00
localStorage.removeItem('passwordResetToken') // Delete an eventually preexisting old token
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
router.push({name: 'user.password-reset.reset'})
2018-11-01 21:34:29 +00:00
}
// Email verification
if (this.$route.query.userEmailConfirm !== undefined) {
localStorage.removeItem('emailConfirmToken') // Delete an eventually preexisting old token
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
router.push({name: 'user.login'})
}
2018-11-01 21:34:29 +00:00
},
beforeCreate() {
this.$store.dispatch('config/update')
this.$store.dispatch('auth/checkAuth')
2020-05-16 10:02:30 +00:00
.then(() => {
// Check if the user is already logged in, if so, redirect them to the homepage
if (
!this.userAuthenticated &&
this.$route.name !== 'user.login' &&
this.$route.name !== 'user.password-reset.request' &&
this.$route.name !== 'user.password-reset.reset' &&
this.$route.name !== 'user.register' &&
this.$route.name !== 'link-share.auth'
2020-05-16 10:02:30 +00:00
) {
router.push({name: 'user.login'})
2020-05-16 10:02:30 +00:00
}
2020-05-16 10:02:30 +00:00
if (this.userAuthenticated && this.userInfo.type === authTypes.USER && (this.$route.params.name === 'home' || this.namespaces.length === 0)) {
this.loadNamespaces()
}
})
},
created() {
// Service worker communication
document.addEventListener(swEvents.SW_UPDATED, this.showRefreshUI, {once: true})
if (navigator && navigator.serviceWorker) {
navigator.serviceWorker.addEventListener(
'controllerchange', () => {
if (this.refreshing) return;
this.refreshing = true;
window.location.reload();
}
)
}
2019-12-19 20:50:07 +00:00
2020-06-25 21:56:41 +00:00
// Hide the menu by default on mobile
if (window.innerWidth < 770) {
this.menuActive = false
}
// Try renewing the token every time vikunja is loaded initially
// (When opening the browser the focus event is not fired)
this.$store.dispatch('auth/renewToken')
// Check if the token is still valid if the window gets focus again to maybe renew it
window.addEventListener('focus', () => {
if (!this.userAuthenticated) {
return
}
const expiresIn = this.userInfo.exp - +new Date() / 1000
// If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page
if (expiresIn < 0) {
this.$store.dispatch('auth/checkAuth')
router.push({name: 'user.login'})
return
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
this.$store.dispatch('auth/renewToken')
console.log('renewed token')
}
})
// This will hide the menu once clicked outside of it
this.$nextTick(() => document.addEventListener('click', () => this.userMenuActive = false))
},
watch: {
// call the method again if the route changes
Kanban (#118) Add error message when trying to create an invalid new task in a bucket Prevent creation of new buckets if the bucket title is empty Disable deleting a bucket if it's the last one Disable dragging tasks when they are being updated Fix transition when opening tasks Send the user to list view by default Show loading spinner when updating multiple tasks Add loading spinner when moving tasks Add loading animation when bucket is loading / updating etc Add bucket title edit Fix creating new buckets Add loading animation Add removing buckets Fix creating a new bucket after tasks were moved Fix warning about labels on tasks Fix labels on tasks not updating after retrieval from api Fix property width Add closing and mobile design Make the task detail popup look good Move list views Move task detail view in a popup Add link to tasks Add saving the new task position after it was moved Fix creating new bucket Fix creating a new task Cleanup Disable user selection for task cards Fix drag placeholder Add dragging style to task Add placeholder + change animation duration More cleanup Cleanup / docs Working of dragging and dropping tasks Adjust markup and styling for new library Change kanban library to something that works Add basic calculation of new positions Don't try to create empty tasks Add indicator if a task is done Add moving tasks between buckets Make empty buckets a little smaller Add gimmick for button description Fix color Fix scrolling bucket layout Add creating a new bucket Add hiding the task input field Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/118
2020-04-25 23:11:34 +00:00
'$route': 'doStuffAfterRoute',
},
computed: mapState({
userInfo: state => state.auth.info,
userAvatar: state => state.auth.avatarUrl,
userAuthenticated: state => state.auth.authenticated,
motd: state => state.config.motd,
online: ONLINE,
fullpage: IS_FULLPAGE,
namespaces(state) {
return state.namespaces.namespaces.filter(n => !n.isArchived)
},
currentList: CURRENT_LIST,
background: 'background',
imprintUrl: state => state.config.legal.imprintUrl,
privacyPolicyUrl: state => state.config.legal.privacyPolicyUrl,
canWriteCurrentList: state => state.currentList.maxRight > Rights.READ,
}),
methods: {
logout() {
this.$store.dispatch('auth/logout')
router.push({name: 'user.login'})
},
loadNamespaces() {
this.$store.dispatch('namespaces/loadNamespaces')
2018-09-09 15:23:06 +00:00
},
loadNamespacesIfNeeded(e) {
if (this.userAuthenticated && (this.userInfo && this.userInfo.type === authTypes.USER) && (e.name === 'home' || this.namespaces.length === 0)) {
this.loadNamespaces()
}
},
2018-12-25 15:03:51 +00:00
doStuffAfterRoute(e) {
// this.setTitle('') // Reset the title if the page component does not set one itself
if (this.$store.state[IS_FULLPAGE]) {
this.$store.commit(IS_FULLPAGE, false)
}
2018-12-25 15:03:51 +00:00
this.loadNamespacesIfNeeded(e)
this.userMenuActive = false
2020-06-25 21:56:41 +00:00
// If the menu is active on desktop, don't hide it because that would confuse the user
if (window.innerWidth < 770) {
this.menuActive = false
}
// Reset the current list highlight in menu if the current list is not list related.
if (
this.$route.name === 'home' ||
this.$route.name === 'namespace.edit' ||
this.$route.name === 'teams.index' ||
this.$route.name === 'teams.edit' ||
this.$route.name === 'tasks.range' ||
this.$route.name === 'labels.index' ||
this.$route.name === 'migrate.start' ||
this.$route.name === 'migrate.wunderlist' ||
this.$route.name === 'user.settings' ||
this.$route.name === 'namespaces.index'
) {
this.$store.commit(CURRENT_LIST, {})
}
2018-12-25 15:03:51 +00:00
},
showRefreshUI(e) {
console.log('recieved refresh event', e)
this.registration = e.detail;
this.updateAvailable = true;
},
refreshApp() {
this.updateExists = false;
if (!this.registration || !this.registration.waiting) {
return;
}
// Notify the service worker to actually do the update
this.registration.waiting.postMessage('skipWaiting');
},
},
}
2018-08-28 20:50:22 +00:00
</script>