feat: defer everything until the api config is loaded #926

Merged
konrad merged 27 commits from feature/ready-state into main 2021-11-13 19:49:03 +00:00
10 changed files with 419 additions and 255 deletions

View File

@ -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

Wrap in <template v-if="authUser">

Wrap in `<template v-if="authUser">`

Why?

Why?

to make clear that it mounts at the same time than the content-auth below.

to make clear that it mounts at the same time than the content-auth below.

Ah, that makes sense. Done.

Ah, that makes sense. Done.
</template>
<content-link-share v-else-if="authLinkShare"/>
<content-no-auth v-else/>
<notification/>
</div>
</div>
konrad marked this conversation as resolved Outdated

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.

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.

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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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

use @

use `@`

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
Review

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?

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?
Review

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?

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?
Review

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 =)
Review

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.

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.
Review

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.
Review

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.
Review

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

Explain why we return here

Explain why we return here

Done.

Done.
this.errorMsg = this.$t('apiConfig.urlRequired')
konrad marked this conversation as resolved Outdated

That seems like it should throw an error?

That seems like it should throw an error?

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
Review

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

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
  • unnest
  • try to not use a tag selector: does using just .url work? if not -> How about .api-url
- unnest - try to not use a tag selector: does using just `.url` work? if not -> How about `.api-url`

The .url isn't used anywhere else. Unnesting and omitting the tag seems to work.

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>

View 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>
konrad marked this conversation as resolved Outdated

Use script setup

Use script setup

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
Review

The min-height should be set from outside.

The min-height should be set from outside.
Review

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).
Review

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.

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.
Review

Opened: #973

Opened: https://kolaente.dev/vikunja/frontend/issues/973
</style>

View 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,
},
konrad marked this conversation as resolved Outdated

start loading in created

start loading in created

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

This error handling should happen inside the loadApp action.

This error handling should happen inside the `loadApp` action.

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

ONLINE is no mutation type. It's a state.

`ONLINE` is no mutation type. It's a state.

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

unnest loader-container

unnest loader-container

Done.

Done.
img {
margin-bottom: 1rem;
konrad marked this conversation as resolved Outdated

&.is-loading::after { (double ::)

`&.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>

View File

@ -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

Since updateConfig is always () => dispatch('config/update'):
Shouldn't we just import he store in this file and call dispatch('config/update') directly?

Since `updateConfig` is always `() => dispatch('config/update')`: Shouldn't we just import he store in this file and call `dispatch('config/update')` directly?

Will that work outside of a vue component?

Will that work outside of a vue component?

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)

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)

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)
})
}

View File

@ -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}.",

View File

@ -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

Use @

Use `@`

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)
},
},
})

View File

@ -1,30 +1,38 @@
// FIXME: move to loading.vue
.loader-container.is-loading {
konrad marked this conversation as resolved Outdated

how about:

// 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;
    }
}
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; } } ```

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;
}
}