feat: defer everything until the api config is loaded (#926)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/926 Reviewed-by: dpschen <dpschen@noreply.kolaente.de> Co-authored-by: konrad <k@knt.li> Co-committed-by: konrad <k@knt.li>
This commit is contained in:
parent
31f0c384ac
commit
0a2d5ef820
10 changed files with 419 additions and 255 deletions
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'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
@ -46,6 +43,7 @@ export default defineComponent({
|
||||||
TopNavigation,
|
TopNavigation,
|
||||||
KeyboardShortcuts,
|
KeyboardShortcuts,
|
||||||
Notification,
|
Notification,
|
||||||
|
Ready,
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.setupOnlineStatus()
|
this.setupOnlineStatus()
|
||||||
|
@ -54,13 +52,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()
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -121,29 +112,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: $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,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 $light-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: $grey-400 !important;
|
color: $grey-400 !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 $primary;
|
border-bottom: 1px dashed $primary;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
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>
|
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>
|
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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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",
|
||||||
|
@ -779,8 +789,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}.",
|
||||||
|
|
|
@ -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,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue