Refactor app component (#283)
Fix redirect to home Move redirect to home to no auth component Move setup stuff to separate functions Renew token in authenticated component Use vue's router object Move auth type checks to computed properties Move after route stuff to authenticated content component More Cleanup Cleanup Hide the navigation on mobile in the navigation component Load namespaces from inside the navigation component Fix logout Move not authenticated content to separate component Fix favoriting lists Move link share authenticated stuff to separate component Move authenticated stuff to separate component Move side navigation to separate component Move top navigation bar to separate component Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/283 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
107
src/components/home/contentAuth.vue
Normal file
107
src/components/home/contentAuth.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div>
|
||||
<a @click="$store.commit('menuActive', false)" class="menu-hide-button" v-if="menuActive">
|
||||
<icon icon="times"></icon>
|
||||
</a>
|
||||
<div
|
||||
:class="{'has-background': background}"
|
||||
:style="{'background-image': `url(${background})`}"
|
||||
class="app-container"
|
||||
>
|
||||
<navigation/>
|
||||
<div
|
||||
:class="[
|
||||
{
|
||||
'fullpage-overlay': fullpage,
|
||||
'is-menu-enabled': menuActive,
|
||||
},
|
||||
$route.name,
|
||||
]"
|
||||
class="app-content"
|
||||
>
|
||||
<a @click="$store.commit('menuActive', false)" class="mobile-overlay" v-if="menuActive"></a>
|
||||
<transition name="fade">
|
||||
<router-view/>
|
||||
</transition>
|
||||
<a @click="$store.commit('keyboardShortcutsActive', true)" class="keyboard-shortcuts-button">
|
||||
<icon icon="keyboard"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import {CURRENT_LIST, IS_FULLPAGE, MENU_ACTIVE} from '@/store/mutation-types'
|
||||
import Navigation from '@/components/home/navigation'
|
||||
|
||||
export default {
|
||||
name: 'contentAuth',
|
||||
components: {Navigation},
|
||||
watch: {
|
||||
'$route': 'doStuffAfterRoute',
|
||||
},
|
||||
created() {
|
||||
this.renewTokenOnFocus()
|
||||
},
|
||||
computed: mapState({
|
||||
fullpage: IS_FULLPAGE,
|
||||
namespaces(state) {
|
||||
return state.namespaces.namespaces.filter(n => !n.isArchived)
|
||||
},
|
||||
currentList: CURRENT_LIST,
|
||||
background: 'background',
|
||||
menuActive: MENU_ACTIVE,
|
||||
}),
|
||||
methods: {
|
||||
doStuffAfterRoute() {
|
||||
// this.setTitle('') // Reset the title if the page component does not set one itself
|
||||
this.$store.commit(IS_FULLPAGE, false)
|
||||
this.resetCurrentList()
|
||||
},
|
||||
resetCurrentList() {
|
||||
// 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, {})
|
||||
}
|
||||
},
|
||||
renewTokenOnFocus() {
|
||||
// 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', () => {
|
||||
|
||||
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')
|
||||
this.$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.debug('renewed token')
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
52
src/components/home/contentLinkShare.vue
Normal file
52
src/components/home/contentLinkShare.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{'has-background': background}"
|
||||
:style="{'background-image': `url(${background})`}"
|
||||
class="link-share-container"
|
||||
>
|
||||
<div class="container has-text-centered link-share-view">
|
||||
<div class="column is-10 is-offset-1">
|
||||
<img alt="Vikunja" class="logo" src="/images/logo-full.svg"/>
|
||||
<h1
|
||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
{{ currentList.title === '' ? 'Loading...' : currentList.title }}
|
||||
</h1>
|
||||
<div class="box has-text-left view">
|
||||
<div class="logout">
|
||||
<a @click="logout()" class="button">
|
||||
<span>Logout</span>
|
||||
<span class="icon is-small">
|
||||
<icon icon="sign-out-alt"/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import {CURRENT_LIST} from '@/store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: 'contentLinkShare',
|
||||
computed: mapState({
|
||||
currentList: CURRENT_LIST,
|
||||
background: 'background',
|
||||
}),
|
||||
methods: {
|
||||
logout() {
|
||||
this.$store.dispatch('auth/logout')
|
||||
this.$router.push({name: 'user.login'})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
42
src/components/home/contentNoAuth.vue
Normal file
42
src/components/home/contentNoAuth.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="noauth-container">
|
||||
<img alt="Vikunja" src="/images/logo-full.svg"/>
|
||||
<div class="message is-info" v-if="motd !== ''">
|
||||
<div class="message-header">
|
||||
<p>Info</p>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{{ motd }}
|
||||
</div>
|
||||
</div>
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'contentNoAuth',
|
||||
created() {
|
||||
this.redirectToHome()
|
||||
},
|
||||
computed: mapState({
|
||||
motd: state => state.config.motd,
|
||||
}),
|
||||
methods: {
|
||||
redirectToHome() {
|
||||
// Check if the user is already logged in and redirect them to the home page if not
|
||||
if (
|
||||
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'
|
||||
) {
|
||||
this.$router.push({name: 'user.login'})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
178
src/components/home/navigation.vue
Normal file
178
src/components/home/navigation.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div :class="{'is-active': menuActive}" class="namespace-container">
|
||||
<div class="menu top-menu">
|
||||
<router-link :to="{name: 'home'}" class="logo">
|
||||
<img alt="Vikunja" src="/images/logo-full.svg"/>
|
||||
</router-link>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<router-link :to="{ name: 'home'}">
|
||||
<span class="icon">
|
||||
<icon icon="calendar"/>
|
||||
</span>
|
||||
Overview
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'tasks.range', params: {type: 'week'}}">
|
||||
<span class="icon">
|
||||
<icon icon="calendar-week"/>
|
||||
</span>
|
||||
Next Week
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'tasks.range', params: {type: 'month'}}">
|
||||
<span class="icon">
|
||||
<icon :icon="['far', 'calendar-alt']"/>
|
||||
</span>
|
||||
Next Month
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'teams.index'}">
|
||||
<span class="icon">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
Teams
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'namespaces.index'}">
|
||||
<span class="icon">
|
||||
<icon icon="layer-group"/>
|
||||
</span>
|
||||
Namespaces & Lists
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: 'labels.index'}">
|
||||
<span class="icon">
|
||||
<icon icon="tags"/>
|
||||
</span>
|
||||
Labels
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<aside class="menu namespaces-lists">
|
||||
<template v-for="n in namespaces">
|
||||
<div :key="n.id">
|
||||
<router-link
|
||||
:to="{name: 'namespace.edit', params: {id: n.id} }"
|
||||
class="nsettings"
|
||||
v-if="n.id > 0"
|
||||
v-tooltip.right="'Settings'">
|
||||
<span class="icon">
|
||||
<icon icon="cog"/>
|
||||
</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:key="n.id + 'list.create'"
|
||||
:to="{ name: 'list.create', params: { id: n.id} }"
|
||||
class="nsettings"
|
||||
v-if="n.id > 0"
|
||||
v-tooltip="'Add a new list in the ' + n.title + ' namespace'">
|
||||
<span class="icon">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
</router-link>
|
||||
<label
|
||||
:for="n.id + 'checker'"
|
||||
class="menu-label"
|
||||
v-tooltip="n.title + ' (' + n.lists.length + ')'">
|
||||
<span class="name">
|
||||
<span
|
||||
:style="{ backgroundColor: n.hexColor }"
|
||||
class="color-bubble"
|
||||
v-if="n.hexColor !== ''">
|
||||
</span>
|
||||
{{ n.title }} ({{ n.lists.length }})
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
:id="n.id + 'checker'"
|
||||
:key="n.id + 'checker'"
|
||||
checked="checked"
|
||||
class="checkinput"
|
||||
type="checkbox"/>
|
||||
<div :key="n.id + 'child'" class="more-container">
|
||||
<ul class="menu-list can-be-hidden">
|
||||
<template v-for="l in n.lists">
|
||||
<!-- This is a bit ugly but vue wouldn't want to let me filter this - probably because the lists
|
||||
are nested inside of the namespaces makes it a lot harder.-->
|
||||
<li :key="l.id" v-if="!l.isArchived">
|
||||
<router-link
|
||||
class="list-menu-link"
|
||||
:class="{'router-link-exact-active': currentList.id === l.id}"
|
||||
:to="{ name: 'list.index', params: { listId: l.id} }"
|
||||
tag="span"
|
||||
>
|
||||
<span
|
||||
:style="{ backgroundColor: l.hexColor }"
|
||||
class="color-bubble"
|
||||
v-if="l.hexColor !== ''">
|
||||
</span>
|
||||
<span class="list-menu-title">
|
||||
{{ l.title }}
|
||||
</span>
|
||||
<span
|
||||
:class="{'is-favorite': l.isFavorite}"
|
||||
@click.stop="toggleFavoriteList(l)"
|
||||
class="favorite">
|
||||
<icon icon="star" v-if="l.isFavorite"/>
|
||||
<icon :icon="['far', 'star']" v-else/>
|
||||
</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<label :for="n.id + 'checker'" class="hidden-hint">
|
||||
Show hidden lists ({{ n.lists.length }})...
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank">Powered by Vikunja</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import {CURRENT_LIST, IS_FULLPAGE, MENU_ACTIVE} from '@/store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: 'navigation',
|
||||
computed: mapState({
|
||||
fullpage: IS_FULLPAGE,
|
||||
namespaces(state) {
|
||||
return state.namespaces.namespaces.filter(n => !n.isArchived)
|
||||
},
|
||||
currentList: CURRENT_LIST,
|
||||
background: 'background',
|
||||
menuActive: MENU_ACTIVE,
|
||||
}),
|
||||
beforeCreate() {
|
||||
this.$store.dispatch('namespaces/loadNamespaces')
|
||||
},
|
||||
created() {
|
||||
// Hide the menu by default on mobile
|
||||
if (window.innerWidth < 770) {
|
||||
this.$store.commit(MENU_ACTIVE, false)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleFavoriteList(list) {
|
||||
// The favorites pseudo list is always favorite
|
||||
// Archived lists cannot be marked favorite
|
||||
if (list.id === -1 || list.isArchived) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('lists/toggleListFavorite', list)
|
||||
.catch(e => this.error(e, this))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
125
src/components/home/topNavigation.vue
Normal file
125
src/components/home/topNavigation.vue
Normal file
@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<nav
|
||||
:class="{'has-background': background}"
|
||||
aria-label="main navigation"
|
||||
class="navbar main-theme is-fixed-top"
|
||||
role="navigation"
|
||||
>
|
||||
<div class="navbar-brand">
|
||||
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||
<img alt="Vikunja" src="/images/logo-full-pride.svg" v-if="(new Date()).getMonth() === 5"/>
|
||||
<img alt="Vikunja" src="/images/logo-full.svg" v-else/>
|
||||
</router-link>
|
||||
<a
|
||||
@click="$store.commit('toggleMenu')"
|
||||
class="menu-show-button"
|
||||
@shortkey="() => $store.commit('toggleMenu')"
|
||||
v-shortkey="['ctrl', 'e']"
|
||||
>
|
||||
<icon icon="bars"></icon>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
@click="$store.commit('toggleMenu')"
|
||||
class="menu-show-button"
|
||||
>
|
||||
<icon icon="bars"></icon>
|
||||
</a>
|
||||
<div class="list-title" v-if="currentList.id">
|
||||
<h1
|
||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
{{ 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>
|
||||
<div class="navbar-end">
|
||||
<update/>
|
||||
<div class="user">
|
||||
<img :src="userAvatar" alt="" class="avatar"/>
|
||||
<div class="dropdown is-right is-active">
|
||||
<div class="dropdown-trigger">
|
||||
<button @click.stop="userMenuActive = !userMenuActive" class="button noshadow">
|
||||
<span class="username">{{ userInfo.username }}</span>
|
||||
<span class="icon is-small">
|
||||
<icon icon="chevron-down"/>
|
||||
</span>
|
||||
</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"
|
||||
class="dropdown-item"
|
||||
target="_blank"
|
||||
v-if="imprintUrl">
|
||||
Imprint
|
||||
</a>
|
||||
<a
|
||||
:href="privacyPolicyUrl"
|
||||
class="dropdown-item"
|
||||
target="_blank"
|
||||
v-if="privacyPolicyUrl">
|
||||
Privacy policy
|
||||
</a>
|
||||
<a @click="keyboardShortcutsActive = true" class="dropdown-item">Keyboard
|
||||
Shortcuts</a>
|
||||
<a @click="logout()" class="dropdown-item">
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
import {CURRENT_LIST} from '@/store/mutation-types'
|
||||
import Rights from '@/models/rights.json'
|
||||
import Update from '@/components/home/update'
|
||||
|
||||
export default {
|
||||
name: 'topNavigation',
|
||||
data() {
|
||||
return {
|
||||
userMenuActive: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Update,
|
||||
},
|
||||
created() {
|
||||
// This will hide the menu once clicked outside of it
|
||||
this.$nextTick(() => document.addEventListener('click', () => this.userMenuActive = false))
|
||||
},
|
||||
computed: mapState({
|
||||
userInfo: state => state.auth.info,
|
||||
userAvatar: state => state.auth.avatarUrl,
|
||||
userAuthenticated: state => state.auth.authenticated,
|
||||
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')
|
||||
this.$router.push({name: 'user.login'})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
49
src/components/home/update.vue
Normal file
49
src/components/home/update.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="update-notification" v-if="updateAvailable">
|
||||
<p>There is an update for Vikunja available!</p>
|
||||
<a @click="refreshApp()" class="button is-primary noshadow">Update Now</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import swEvents from '@/ServiceWorker/events.json'
|
||||
|
||||
export default {
|
||||
name: 'update',
|
||||
data() {
|
||||
return {
|
||||
updateAvailable: false,
|
||||
registration: null,
|
||||
refreshing: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
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()
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -88,11 +88,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: 'keyboard-shortcuts',
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close')
|
||||
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export default {
|
||||
params = null,
|
||||
) {
|
||||
|
||||
// Because this function is triggered every time on navigation, we're putting a condition here to only load it when we actually want to show tasks
|
||||
// Because this function is triggered every time on topNavigation, we're putting a condition here to only load it when we actually want to show tasks
|
||||
// FIXME: This is a bit hacky -> Cleanup.
|
||||
if (
|
||||
this.$route.name !== 'list.list' &&
|
||||
|
Reference in New Issue
Block a user