diff --git a/app/src/App.vue b/app/src/App.vue
index eaf8eee3..d310d4d3 100644
--- a/app/src/App.vue
+++ b/app/src/App.vue
@@ -1,23 +1,16 @@
-
-
@@ -19,12 +12,19 @@ const { breadcrumb } = useStoreGetters()
{{ text }}
+
+
diff --git a/app/src/components/layouts/MainLayout.vue b/app/src/components/layouts/MainLayout.vue
index 9f55d2e1..c3fb19bd 100644
--- a/app/src/components/layouts/MainLayout.vue
+++ b/app/src/components/layouts/MainLayout.vue
@@ -10,13 +10,13 @@ import {
ModalWaiting,
ModalWarning,
} from '@/components/modals'
+import { useInfos } from '@/composables/useInfos'
import { useRequests } from '@/composables/useRequests'
import { useSettings } from '@/composables/useSettings'
-import { useStoreGetters } from '@/store/utils'
import type { VueClass } from '@/types/commons'
const router = useRouter()
-const { routerKey } = useStoreGetters()
+const { routerKey } = useInfos()
const { reconnecting, currentRequest, dismissModal } = useRequests()
const { transitions, transitionName, dark } = useSettings()
diff --git a/app/src/composables/useInfos.ts b/app/src/composables/useInfos.ts
index 17b95b94..46891d9e 100644
--- a/app/src/composables/useInfos.ts
+++ b/app/src/composables/useInfos.ts
@@ -1,212 +1,189 @@
+import { createGlobalState, useLocalStorage } from '@vueuse/core'
+import { computed, ref } from 'vue'
+import type {
+ RouteLocationNormalized,
+ RouteLocationNormalizedLoaded,
+ RouteRecordNormalized,
+} from 'vue-router'
+import { useRouter } from 'vue-router'
+
import api from '@/api'
-import { useRequests, type ReconnectingArgs } from '@/composables/useRequests'
import { isEmptyValue, timeout } from '@/helpers/commons'
import i18n from '@/i18n'
-import router from '@/router'
+import { useStoreGetters } from '@/store/utils'
+import type { CustomRoute, RouteFromTo } from '@/types/commons'
+import { useRequests, type ReconnectingArgs } from './useRequests'
-export default {
- state: {
- host: window.location.host, // String
- installed: null,
- connected: localStorage.getItem('connected') === 'true', // Boolean
- yunohost: null, // Object { version, repo }
- routerKey: undefined, // String if current route has params
- breadcrumb: [], // Array of routes
- transitionName: null, // String of CSS class if transitions are enabled
- },
+export const useInfos = createGlobalState(() => {
+ const router = useRouter()
- mutations: {
- SET_INSTALLED(state, boolean) {
- state.installed = boolean
- },
+ const host = ref(window.location.host)
+ const installed = ref()
+ const connected = useLocalStorage('connected', false)
+ const yunohost = ref<{ version: string; repo: string } | undefined>()
+ const routerKey = ref()
+ const breadcrumb = ref([])
- SET_CONNECTED(state, boolean) {
- localStorage.setItem('connected', boolean)
- state.connected = boolean
- },
+ const { mainDomain } = useStoreGetters()
+ const ssoLink = computed(() => {
+ return `//${mainDomain.value ?? host.value}/yunohost/sso`
+ })
- SET_YUNOHOST_INFOS(state, yunohost) {
- state.yunohost = yunohost
- },
+ // INIT
- SET_ROUTER_KEY(state, key) {
- state.routerKey = key
- },
-
- SET_BREADCRUMB(state, breadcrumb) {
- state.breadcrumb = breadcrumb
- },
-
- SET_TRANSITION_NAME(state, transitionName) {
- state.transitionName = transitionName
- },
- },
-
- actions: {
- async ON_APP_CREATED({ dispatch, state }) {
- await dispatch('CHECK_INSTALL')
-
- if (!state.installed) {
- router.push({ name: 'post-install' })
- } else {
- dispatch('CONNECT')
- }
- },
-
- async CHECK_INSTALL({ dispatch, commit }, retry = 2) {
- // this action will try to query the `/installed` route 3 times every 5 s with
- // a timeout of the same delay.
- // FIXME need testing with api not responding
- try {
- const { installed } = await timeout(api.get('installed'), 5000)
- commit('SET_INSTALLED', installed)
- return installed
- } catch (err) {
- if (retry > 0) {
- return dispatch('CHECK_INSTALL', --retry)
- }
- throw err
- }
- },
-
- async CONNECT({ commit, dispatch }) {
- // If the user is not connected, the first action will throw
- // and login prompt will be shown automaticly
- await dispatch('GET_YUNOHOST_INFOS')
- commit('SET_CONNECTED', true)
- await api.get({ uri: 'domains', storeKey: 'domains' })
- },
-
- RESET_CONNECTED({ commit }) {
- commit('SET_CONNECTED', false)
- commit('SET_YUNOHOST_INFOS', null)
- },
-
- DISCONNECT({ dispatch }, route) {
- // FIXME vue3 currentRoute is now a ref (currentRoute.value)
- dispatch('RESET_CONNECTED')
- if (router.currentRoute.value.name === 'login') return
- const previousRoute = route ?? router.currentRoute.value
- router.push({
- name: 'login',
- // Add a redirect query if next route is not unknown (like `logout`) or `login`
- query:
- previousRoute && !['login', null].includes(previousRoute.name)
- ? { redirect: previousRoute.path }
- : {},
- })
- },
-
- LOGIN({ dispatch }, credentials) {
- return api
- .post('login', { credentials }, null, { websocket: false })
- .then(() => {
- return dispatch('CONNECT')
- })
- },
-
- LOGOUT({ dispatch }) {
- dispatch('DISCONNECT')
- return api.get('logout')
- },
-
- TRY_TO_RECONNECT({ commit }, args?: ReconnectingArgs) {
- // FIXME This is very ugly arguments forwarding, will use proper component way of doing this when switching to Vue 3 (teleport)
- useRequests().reconnecting.value = args
- },
-
- GET_YUNOHOST_INFOS({ commit }) {
- return api.get('versions').then((versions) => {
- commit('SET_YUNOHOST_INFOS', versions.yunohost)
- })
- },
-
- UPDATE_ROUTER_KEY({ commit }, { to, from }) {
- if (isEmptyValue(to.params)) {
- commit('SET_ROUTER_KEY', undefined)
- return
- }
- // If the next route uses the same component as the previous one, Vue will not
- // recreate an instance of that component, so hooks like `created()` will not be
- // triggered and data will not be fetched.
- // For routes with params, we create a unique key to force the recreation of a view.
- // Params can be declared in route `meta` to stricly define which params should be
- // taken into account.
- const params = to.meta.routerParams
- ? to.meta.routerParams.map((key) => to.params[key])
- : Object.values(to.params)
-
- commit('SET_ROUTER_KEY', `${to.name}-${params.join('-')}`)
- },
-
- UPDATE_BREADCRUMB({ commit }, { to, from }) {
- function getRouteNames(route) {
- if (route.meta.breadcrumb) return route.meta.breadcrumb
- const parentRoute = route.matched
- .slice()
- .reverse()
- .find((route) => route.meta.breadcrumb)
- if (parentRoute) return parentRoute.meta.breadcrumb
- return []
- }
-
- function formatRoute(route) {
- const { trad, param } = route.meta.args || {}
- let text = ''
- // if a traduction key string has been given and we also need to pass
- // the route param as a variable.
- if (trad && param) {
- text = i18n.global.t(trad, { [param]: to.params[param] })
- } else if (trad) {
- text = i18n.global.t(trad)
- } else {
- text = to.params[param]
- }
- return { name: route.name, text }
- }
-
- const routeNames = getRouteNames(to)
- const allRoutes = router.getRoutes()
- const breadcrumb = routeNames.map((name) => {
- const route = allRoutes.find((route) => route.name === name)
- return formatRoute(route)
- })
-
- commit('SET_BREADCRUMB', breadcrumb)
-
- function getTitle(breadcrumb) {
- if (breadcrumb.length === 0) return formatRoute(to).text
- return (breadcrumb.length > 2 ? breadcrumb.slice(-2) : breadcrumb)
- .map((route) => route.text)
- .reverse()
- .join(' / ')
- }
-
- // Display a simplified breadcrumb as the document title.
- document.title = `${getTitle(breadcrumb)} | ${i18n.global.t('yunohost_admin')}`
- },
-
- UPDATE_TRANSITION_NAME({ state, commit }, { to, from }) {
- // Use the breadcrumb array length as a direction indicator
- const toDepth = (to.meta.breadcrumb || []).length
- const fromDepth = (from.meta.breadcrumb || []).length
- commit(
- 'SET_TRANSITION_NAME',
- toDepth < fromDepth ? 'slide-right' : 'slide-left',
+ async function _checkInstall(retry = 2) {
+ // this action will try to query the `/installed` route 3 times every 5 s with
+ // a timeout of the same delay.
+ // FIXME need testing with api not responding
+ try {
+ const data = await timeout(
+ api.get<{ installed: boolean }>('installed'),
+ 5000,
)
- },
- },
+ installed.value = data.installed
+ } catch (err) {
+ if (retry > 0) {
+ return _checkInstall(--retry)
+ }
+ throw err
+ }
+ }
- getters: {
- host: (state) => state.host,
- installed: (state) => state.installed,
- connected: (state) => state.connected,
- yunohost: (state) => state.yunohost,
- routerKey: (state) => state.routerKey,
- breadcrumb: (state) => state.breadcrumb,
- transitionName: (state) => state.transitionName,
- ssoLink: (state, getters) => {
- return `//${getters.mainDomain ?? state.host}/yunohost/sso`
- },
- },
-}
+ async function onAppCreated() {
+ await _checkInstall()
+
+ if (!installed.value) {
+ router.push({ name: 'post-install' })
+ } else {
+ _onLogin()
+ }
+ }
+
+ function getYunoHostVersion() {
+ return api.get('versions').then((versions) => {
+ yunohost.value = versions.yunohost
+ })
+ }
+
+ // CONNECTION
+
+ async function _onLogin() {
+ // If the user is not connected, the first action will throw
+ // and login prompt will be shown automaticly
+ await getYunoHostVersion()
+ connected.value = true
+ await api.get({ uri: 'domains', cachePath: 'domainList' })
+ }
+
+ function onLogout(route?: RouteLocationNormalizedLoaded) {
+ connected.value = false
+ yunohost.value = undefined
+ const previousRoute = route ?? router.currentRoute.value
+ if (previousRoute.name === 'login') return
+ router.push({
+ name: 'login',
+ // Add a redirect query if next route is not unknown (like `logout`) or `login`
+ query:
+ previousRoute && !['login', null].includes(previousRoute.name as any)
+ ? { redirect: previousRoute.path }
+ : {},
+ })
+ }
+
+ function login(credentials: string) {
+ return api
+ .post({ uri: 'login', data: { credentials }, websocket: false })
+ .then(() => _onLogin())
+ }
+
+ function logout() {
+ onLogout()
+ return api.get('logout')
+ }
+
+ function tryToReconnect(args?: ReconnectingArgs) {
+ useRequests().reconnecting.value = args
+ }
+
+ function updateRouterKey({ to }: RouteFromTo) {
+ if (isEmptyValue(to.params)) {
+ routerKey.value = undefined
+ return
+ }
+ // If the next route uses the same component as the previous one, Vue will not
+ // recreate an instance of that component, so hooks like `created()` will not be
+ // triggered and data will not be fetched.
+ // For routes with params, we create a unique key to force the recreation of a view.
+ // Params can be declared in route `meta` to stricly define which params should be
+ // taken into account.
+ const params = to.meta.routerParams
+ ? to.meta.routerParams.map((key) => to.params[key])
+ : Object.values(to.params)
+
+ routerKey.value = `${to.name?.toString()}-${params.join('-')}`
+ }
+
+ function updateBreadcrumb({ to }: RouteFromTo) {
+ function getRouteNames(route: RouteLocationNormalized): string[] {
+ if (route.meta.breadcrumb) return route.meta.breadcrumb
+ const parentRoute = route.matched
+ .slice()
+ .reverse()
+ .find((route) => route.meta.breadcrumb)
+ return parentRoute?.meta.breadcrumb || []
+ }
+
+ function formatRoute(
+ route: RouteRecordNormalized | RouteLocationNormalized,
+ ) {
+ const { trad, param } = route.meta.args || {}
+ let text = ''
+ // if a traduction key string has been given and we also need to pass
+ // the route param as a variable.
+ if (trad && param) {
+ text = i18n.global.t(trad, { [param]: to.params[param] })
+ } else if (trad) {
+ text = i18n.global.t(trad)
+ } else if (param) {
+ text = to.params[param] as string
+ }
+ return { to: { name: route.name! }, text }
+ }
+
+ const routeNames = getRouteNames(to)
+ const allRoutes = router.getRoutes()
+ breadcrumb.value = routeNames.map((name) => {
+ const route = allRoutes.find((route) => route.name === name)!
+ return formatRoute(route)
+ })
+
+ function getTitle(breadcrumb: CustomRoute[]) {
+ if (breadcrumb.length === 0) return formatRoute(to).text
+ return (breadcrumb.length > 2 ? breadcrumb.slice(-2) : breadcrumb)
+ .map((route) => route.text)
+ .reverse()
+ .join(' / ')
+ }
+
+ // Display a simplified breadcrumb as the document title.
+ document.title = `${getTitle(breadcrumb.value)} | ${i18n.global.t('yunohost_admin')}`
+ }
+
+ return {
+ host,
+ installed,
+ connected,
+ yunohost,
+ routerKey,
+ breadcrumb,
+ ssoLink,
+ onAppCreated,
+ getYunoHostVersion,
+ onLogout,
+ login,
+ logout,
+ tryToReconnect,
+ updateRouterKey,
+ updateBreadcrumb,
+ }
+})
diff --git a/app/src/composables/useRequests.ts b/app/src/composables/useRequests.ts
index 7d6a08f7..52cafd37 100644
--- a/app/src/composables/useRequests.ts
+++ b/app/src/composables/useRequests.ts
@@ -7,8 +7,8 @@ import type { APIQuery, RequestMethod } from '@/api/api'
import { APIErrorLog, type APIError } from '@/api/errors'
import { isObjectLiteral } from '@/helpers/commons'
import i18n from '@/i18n'
-import store from '@/store'
import type { StateVariant } from '@/types/commons'
+import { useInfos } from './useInfos'
export type RequestStatus = 'pending' | 'success' | 'warning' | 'error'
@@ -162,7 +162,7 @@ export const useRequests = createGlobalState(() => {
err.log()
if (err.code === 401) {
// Unauthorized
- store.dispatch('DISCONNECT')
+ useInfos().onLogout()
} else if (err instanceof APIErrorLog) {
// Errors that have produced logs
router.push({ name: 'tool-log', params: { name: err.logRef } })
diff --git a/app/src/router/index.ts b/app/src/router/index.ts
index 95b7e1e9..898c2e07 100644
--- a/app/src/router/index.ts
+++ b/app/src/router/index.ts
@@ -1,8 +1,8 @@
import { createRouter, createWebHashHistory } from 'vue-router'
+import { useInfos } from '@/composables/useInfos'
import { useRequests } from '@/composables/useRequests'
import { useSettings } from '@/composables/useSettings'
-import store from '@/store'
import routes from './routes'
const router = createRouter({
@@ -39,20 +39,22 @@ router.beforeEach((to, from, next) => {
dismissModal(currentRequest.value.id)
}
- if (to.name === 'post-install' && store.getters.installed) {
+ const { installed, connected, onLogout } = useInfos()
+ if (to.name === 'post-install' && installed.value) {
return next('/')
}
// Allow if connected or route is not protected
- if (store.getters.connected || to.meta.noAuth) {
+ if (connected.value || to.meta.noAuth) {
next()
} else {
- store.dispatch('DISCONNECT', to)
+ onLogout(to)
}
})
router.afterEach((to, from) => {
- store.dispatch('UPDATE_ROUTER_KEY', { to, from })
- store.dispatch('UPDATE_BREADCRUMB', { to, from })
+ const { updateRouterKey, updateBreadcrumb } = useInfos()
+ updateRouterKey({ to, from })
+ updateBreadcrumb({ to, from })
})
export default router
diff --git a/app/src/views/LoginView.vue b/app/src/views/LoginView.vue
index 75babf7e..bd212679 100644
--- a/app/src/views/LoginView.vue
+++ b/app/src/views/LoginView.vue
@@ -2,11 +2,10 @@
import { reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter, type LocationQueryValue } from 'vue-router'
-import { useStore } from 'vuex'
import { useForm } from '@/composables/form'
+import { useInfos } from '@/composables/useInfos'
import { alphalownumdot_, minLength, required } from '@/helpers/validators'
-import { useStoreGetters } from '@/store/utils'
import type { FieldProps, FormFieldDict } from '@/types/form'
const props = withDefaults(
@@ -19,10 +18,8 @@ const props = withDefaults(
)
const { t } = useI18n()
-const store = useStore()
const router = useRouter()
-
-const { installed } = useStoreGetters()
+const { login, installed } = useInfos()
type Form = typeof form.value
const form = ref({
@@ -57,8 +54,7 @@ const { v, onSubmit } = useForm(form, fields)
const onLogin = onSubmit((onError) => {
const { username, password } = form.value
const credentials = [username, password].join(':')
- store
- .dispatch('LOGIN', credentials)
+ login(credentials)
.then(() => {
if (props.forceReload) {
window.location.href = '/yunohost/admin/'
diff --git a/app/src/views/backup/BackupInfo.vue b/app/src/views/backup/BackupInfo.vue
index ece48c95..47ee2b19 100644
--- a/app/src/views/backup/BackupInfo.vue
+++ b/app/src/views/backup/BackupInfo.vue
@@ -2,11 +2,11 @@
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
-import { useStore } from 'vuex'
import api from '@/api'
import { APIBadRequestError, type APIError } from '@/api/errors'
import { useAutoModal } from '@/composables/useAutoModal'
+import { useInfos } from '@/composables/useInfos'
import { useInitialQueries } from '@/composables/useInitialQueries'
import { isEmptyValue } from '@/helpers/commons'
import { readableDate } from '@/helpers/filters/date'
@@ -19,7 +19,6 @@ const props = defineProps<{
const { t } = useI18n()
const router = useRouter()
-const store = useStore()
const modalConfirm = useAutoModal()
const { loading } = useInitialQueries(
[{ uri: `backups/${props.name}?with_details` }],
@@ -128,9 +127,9 @@ async function deleteBackup() {
}
function downloadBackup() {
- const host = store.getters.host
+ const { host } = useInfos()
window.open(
- `https://${host}/yunohost/api/backups/${props.name}/download`,
+ `https://${host.value}/yunohost/api/backups/${props.name}/download`,
'_blank',
)
}
diff --git a/app/src/views/tool/ToolPower.vue b/app/src/views/tool/ToolPower.vue
index f89250a0..31b0e348 100644
--- a/app/src/views/tool/ToolPower.vue
+++ b/app/src/views/tool/ToolPower.vue
@@ -1,13 +1,13 @@
diff --git a/app/src/views/update/SystemUpdate.vue b/app/src/views/update/SystemUpdate.vue
index 3c50d6c1..7e300a19 100644
--- a/app/src/views/update/SystemUpdate.vue
+++ b/app/src/views/update/SystemUpdate.vue
@@ -1,15 +1,15 @@