feat: defer everything until the api config is loaded #926
69
src/App.vue
|
@ -1,25 +1,21 @@
|
|||
<template>
|
||||
<div :class="{'is-touch': isTouch}">
|
||||
<div :class="{'is-hidden': !online}">
|
||||
<!-- 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>
|
||||
<top-navigation v-if="authUser"/>
|
||||
<content-auth v-if="authUser"/>
|
||||
<content-link-share v-else-if="authLinkShare"/>
|
||||
<content-no-auth v-else/>
|
||||
<notification/>
|
||||
</div>
|
||||
<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>
|
||||
<ready>
|
||||
<div :class="{'is-touch': isTouch}">
|
||||
<div :class="{'is-hidden': !online}">
|
||||
<template v-if="authUser">
|
||||
<top-navigation/>
|
||||
<content-auth/>
|
||||
konrad marked this conversation as resolved
Outdated
|
||||
</template>
|
||||
<content-link-share v-else-if="authLinkShare"/>
|
||||
<content-no-auth v-else/>
|
||||
<notification/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Didn't test, but just from reading: Else the api might not be reached because we are offline. => slot is never exposed. Didn't test, but just from reading:
might it be that the not offline check needs to be outside of `<ready>`?
Else the api might not be reached because we are offline. => slot is never exposed.
konrad
commented
I think that's pretty much the case. Moved the offline overlay to the ready component. I think that's pretty much the case. Moved the offline overlay to the ready component.
|
||||
<transition name="fade">
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
</transition>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
</transition>
|
||||
</div>
|
||||
</ready>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -36,6 +32,7 @@ import ContentLinkShare from './components/home/contentLinkShare'
|
|||
import ContentNoAuth from './components/home/contentNoAuth'
|
||||
import {setLanguage} from './i18n'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import Ready from '@/components/misc/ready'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'app',
|
||||
|
@ -46,6 +43,7 @@ export default defineComponent({
|
|||
TopNavigation,
|
||||
KeyboardShortcuts,
|
||||
Notification,
|
||||
Ready,
|
||||
},
|
||||
beforeMount() {
|
||||
this.setupOnlineStatus()
|
||||
|
@ -54,13 +52,6 @@ export default defineComponent({
|
|||
this.setupAccountDeletionVerification()
|
||||
},
|
||||
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()
|
||||
},
|
||||
created() {
|
||||
|
@ -121,29 +112,3 @@ export default defineComponent({
|
|||
<style lang="scss">
|
||||
@import '@/styles/global.scss';
|
||||
</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>
|
||||
<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>
|
||||
<router-view/>
|
||||
</div>
|
||||
</div>
|
||||
<no-auth-wrapper>
|
||||
<router-view/>
|
||||
</no-auth-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
|
||||
import { saveLastVisited } from '@/helpers/saveLastVisited'
|
||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
||||
|
||||
export default {
|
||||
name: 'contentNoAuth',
|
||||
components: { Logo },
|
||||
components: {NoAuthWrapper},
|
||||
computed: {
|
||||
routeName() {
|
||||
return this.$route.name
|
||||
},
|
||||
...mapState({
|
||||
motd: state => state.config.motd,
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
routeName: {
|
||||
|
@ -62,17 +45,3 @@ export default {
|
|||
},
|
||||
}
|
||||
</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>
|
||||
</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" >
|
||||
<div class="namespace-title" :class="{'has-menu': n.id > 0}">
|
||||
<span
|
||||
|
@ -105,7 +105,7 @@
|
|||
>
|
||||
<template #item="{element: l}">
|
||||
<li
|
||||
class="loader-container"
|
||||
class="loader-container is-loading-small"
|
||||
:class="{'is-loading': listUpdating[l.id]}"
|
||||
>
|
||||
<router-link
|
||||
|
@ -449,14 +449,6 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
&:hover :deep(.dropdown-trigger) {
|
||||
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 {
|
||||
|
@ -533,14 +525,6 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
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 {
|
||||
color: $grey-400 !important;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<i18n-t keypath="apiConfig.signInOn">
|
||||
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
|
||||
</i18n-t>
|
||||
<br />
|
||||
<br/>
|
||||
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a>
|
||||
</div>
|
||||
|
||||
|
@ -46,9 +46,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { parseURL } from 'ufo'
|
||||
|
||||
const API_DEFAULT_PORT = 3456
|
||||
import {parseURL} from 'ufo'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
use use `@`
konrad
commented
Done. Done.
|
||||
|
||||
export default {
|
||||
name: 'apiConfig',
|
||||
|
@ -71,128 +70,48 @@ export default {
|
|||
return parseURL(this.apiUrl).host
|
||||
},
|
||||
},
|
||||
props: {
|
||||
configureOpen: {
|
||||
dpschen marked this conversation as resolved
dpschen
commented
I was first confused by the naming of this prop. I was first confused by the naming of this prop.
Since the component is just use once and if used we set this to true:
shall we just remove it to simplify this?
konrad
commented
The How would you simplify it? The `<api-config>` component is used in two places (ready and login view) and this prop is only used in one of those. Just removing the prop does not seem to be a good option here.
How would you simplify it?
dpschen
commented
Can you explain why api-config is also in Login.vue? I think I didn't get that =) Can you explain why api-config is also in Login.vue? I think I didn't get that =)
konrad
commented
Mostly to be able to change the url on the login screen, even if one is already defined. It also shows the user what api server they are connecting to. The login screen is the first entry point, but really this should be on the other I'm planning a follow-up PR to refactor the whole noauth thing a bit, will include that there. Mostly to be able to change the url on the login screen, even if one is already defined. It also shows the user what api server they are connecting to.
The login screen is the first entry point, but really this should be on the other `noauth` screens as well (Register, Password reset etc).
I'm planning a follow-up PR to refactor the whole noauth thing a bit, will include that there.
dpschen
commented
I'm a bit afraid that I'll never be able to merge the modals branch #816 if we don't plan ahead. Maybe we can align those changes you still want to do or built them after merge on top of the modals branch. I'm a bit afraid that I'll never be able to merge the modals branch https://kolaente.dev/vikunja/frontend/pulls/816 if we don't plan ahead.
Maybe we can align those changes you still want to do or built them after merge on top of the modals branch.
konrad
commented
Sure. It should be fine to do them after the modals branch is done. Sure. It should be fine to do them after the modals branch is done.
dpschen
commented
Let's resolve this and merge this branch. Then I can try to merge it into #816 and we can think about how to continue =) Let's resolve this and merge this branch. Then I can try to merge it into #816 and we can think about how to continue =)
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
configureOpen: {
|
||||
handler(value) {
|
||||
this.configureApi = value
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setApiUrl() {
|
||||
async setApiUrl() {
|
||||
if (this.apiUrl === '') {
|
||||
// Don't try to check and set an empty url
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Explain why we return here Explain why we return here
konrad
commented
Done. Done.
|
||||
this.errorMsg = this.$t('apiConfig.urlRequired')
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
That seems like it should throw an error? That seems like it should throw an error?
konrad
commented
It does yeah. I've modified it so it shows an error with a "good" error message to the user instead of throwing one. It does yeah. I've modified it so it shows an error with a "good" error message to the user instead of throwing one.
|
||||
return
|
||||
}
|
||||
|
||||
let urlToCheck = this.apiUrl
|
||||
try {
|
||||
const url = await checkAndSetApiUrl(this.apiUrl)
|
||||
|
||||
konrad marked this conversation as resolved
dpschen
commented
Explain why we return here Explain why we return here
|
||||
// Check if the url has an http prefix
|
||||
if (
|
||||
!urlToCheck.startsWith('http://') &&
|
||||
!urlToCheck.startsWith('https://')
|
||||
) {
|
||||
urlToCheck = `http://${urlToCheck}`
|
||||
if (url === '') {
|
||||
// If the config setter function could not figure out a url
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
That seems like it should throw an error? (2) That seems like it should throw an error? (2)
|
||||
throw new Error('URL cannot be empty.')
|
||||
}
|
||||
|
||||
// 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>
|
||||
.api-config {
|
||||
margin-bottom: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.api-url-info {
|
||||
font-size: .9rem;
|
||||
text-align: right;
|
||||
font-size: .9rem;
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
- unnest
- try to not use a tag selector: does using just `.url` work? if not -> How about `.api-url`
konrad
commented
The The `.url` isn't used anywhere else. Unnesting and omitting the tag seems to work.
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
span.url {
|
||||
border-bottom: 1px dashed $primary;
|
||||
}
|
||||
.url {
|
||||
border-bottom: 1px dashed $primary;
|
||||
}
|
||||
</style>
|
|
@ -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>
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Use script setup Use script setup
konrad
commented
Makes a lot of sense. Done! Makes a lot of sense. Done!
|
||||
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;
|
||||
}
|
||||
konrad marked this conversation as resolved
dpschen
commented
The min-height should be set from outside. The min-height should be set from outside.
konrad
commented
And the background as well? Because the background will only work with a height of 100vh (to keep the llama at the bottom). And the background as well? Because the background will only work with a height of 100vh (to keep the llama at the bottom).
dpschen
commented
Would probably be best to import the llama image as component with the new vite-svg-loader in the parent component. Would probably be best to import the llama image as component with the new vite-svg-loader in the parent component.
Then it can be positioned absolute.
Maybe add a new issue and we resolve that after merging. Because there will be conflicts and I think that's hard to solve before.
konrad
commented
Opened: #973 Opened: https://kolaente.dev/vikunja/frontend/issues/973
|
||||
</style>
|
|
@ -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,
|
||||
},
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
start loading in created start loading in created
konrad
commented
Done. Done.
|
||||
data() {
|
||||
return {
|
||||
logoUrl,
|
||||
error: '',
|
||||
errorNoApiUrl: ERROR_NO_API_URL,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.load()
|
||||
},
|
||||
computed: {
|
||||
ready() {
|
||||
return this.$store.state.vikunjaReady
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
This error handling should happen inside the This error handling should happen inside the `loadApp` action.
konrad
commented
Actually, I've simplified this so that the check does not need to happen here. Actually, I've simplified this so that the check does not need to happen here.
|
||||
},
|
||||
showLoading() {
|
||||
return !this.ready && this.error === ''
|
||||
},
|
||||
...mapState([
|
||||
'online',
|
||||
]),
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
`ONLINE` is no mutation type. It's a state.
konrad
commented
Right, fixed. Right, fixed.
|
||||
},
|
||||
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;
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
unnest loader-container unnest loader-container
konrad
commented
Done. Done.
|
||||
|
||||
img {
|
||||
margin-bottom: 1rem;
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
`&.is-loading::after {` (double `::`)
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
|
@ -0,0 +1,118 @@
|
|||
import {store} from '@/store'
|
||||
|
||||
const API_DEFAULT_PORT = '3456'
|
||||
|
||||
export const ERROR_NO_API_URL = 'noApiUrlProvided'
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Since Since `updateConfig` is always `() => dispatch('config/update')`:
Shouldn't we just import he store in this file and call `dispatch('config/update')` directly?
konrad
commented
Will that work outside of a vue component? Will that work outside of a vue component?
dpschen
commented
If you import the store with:
Because it's JS ™️ (trying to steal reacts claim here) If you import the store with:
```
import {store} from '@/store'
// then you can
store.dispatch('config/update')
```
Because it's JS ™️ (trying to steal reacts claim here)
konrad
commented
That seems like a nice way to solve it. Done. That seems like a nice way to solve it. Done.
|
||||
|
||||
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",
|
||||
"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": {
|
||||
"auth": {
|
||||
"username": "Username",
|
||||
|
@ -778,8 +788,9 @@
|
|||
"urlPlaceholder": "eg. https://localhost:3456",
|
||||
"change": "change",
|
||||
"signInOn": "Sign in to your Vikunja account on {0}",
|
||||
"error": "Could not find or use Vikunja installation at \"{domain}\".",
|
||||
"success": "Using 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}\".",
|
||||
"urlRequired": "A url is required."
|
||||
},
|
||||
"loadingError": {
|
||||
"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 ListService from '../services/list'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
Use Use `@`
konrad
commented
Done. Done.
|
||||
|
||||
export const store = createStore({
|
||||
strict: import.meta.env.DEV,
|
||||
|
@ -43,6 +44,7 @@ export const store = createStore({
|
|||
menuActive: true,
|
||||
keyboardShortcutsActive: false,
|
||||
quickActionsActive: false,
|
||||
vikunjaReady: false,
|
||||
},
|
||||
mutations: {
|
||||
[LOADING](state, loading) {
|
||||
|
@ -84,6 +86,9 @@ export const store = createStore({
|
|||
[BACKGROUND](state, background) {
|
||||
state.background = background
|
||||
},
|
||||
vikunjaReady(state, ready) {
|
||||
state.vikunjaReady = ready
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async [CURRENT_LIST]({state, commit}, currentList) {
|
||||
|
@ -138,5 +143,10 @@ export const store = createStore({
|
|||
|
||||
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
|
||||
.loader-container.is-loading {
|
||||
konrad marked this conversation as resolved
Outdated
dpschen
commented
how about:
how about:
```scss
// FIXME: move to loading.vue
.loader-container.is-loading {
position: relative;
pointer-events: none;
opacity: 0.5;
&::after {
@include loader;
position: absolute;
top: calc(50% - 2.5rem);
left: calc(50% - 2.5rem);
width: 5rem;
height: 5rem;
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;
}
}
```
konrad
commented
Makes sense. Done. Makes sense. Done.
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
|
||||
&::after {
|
||||
@include loader;
|
||||
position: absolute;
|
||||
top: calc(50% - 2.5rem);
|
||||
left: calc(50% - 2.5rem);
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-width: 0.25rem;
|
||||
}
|
||||
&::after {
|
||||
@include loader;
|
||||
position: absolute;
|
||||
top: calc(50% - 2.5rem);
|
||||
left: calc(50% - 2.5rem);
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
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
|
||||
.spinner.is-loading {
|
||||
pointer-events: none;
|
||||
pointer-events: none;
|
||||
|
||||
&::after {
|
||||
@include loader;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: calc(50% - 1rem);
|
||||
margin-top: 1rem;
|
||||
border-width: 0.25rem;
|
||||
}
|
||||
&::after {
|
||||
@include loader;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: calc(50% - 1rem);
|
||||
margin-top: 1rem;
|
||||
border-width: 0.25rem;
|
||||
}
|
||||
}
|
Wrap in
<template v-if="authUser">
Why?
to make clear that it mounts at the same time than the content-auth below.
Ah, that makes sense. Done.