feat: use script setup and ts in app auth components
This commit is contained in:
parent
b03d5d80cd
commit
c3c4d2a0a5
9 changed files with 278 additions and 301 deletions
|
@ -23,6 +23,7 @@
|
|||
"@sentry/vue": "6.16.1",
|
||||
"@vue/compat": "3.2.26",
|
||||
"@vueuse/core": "7.3.0",
|
||||
"@vueuse/router": "^7.3.0",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.64.0",
|
||||
|
|
157
src/App.vue
157
src/App.vue
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<ready>
|
||||
<div :class="{'is-touch': isTouch}">
|
||||
<ready :class="{'is-touch': isTouch}">
|
||||
<div :class="{'is-hidden': !online}">
|
||||
<template v-if="authUser">
|
||||
<top-navigation/>
|
||||
|
@ -14,112 +13,84 @@
|
|||
<transition name="fade">
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
</transition>
|
||||
</div>
|
||||
</ready>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from 'vue'
|
||||
import {mapState, mapGetters} from 'vuex'
|
||||
<script lang="ts" setup>
|
||||
import {computed, watch, watchEffect, Ref} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
import {useStore} from 'vuex'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useOnline} from '@vueuse/core'
|
||||
import isTouchDevice from 'is-touch-device'
|
||||
import {success} from '@/message'
|
||||
|
||||
import Notification from '@/components/misc/notification.vue'
|
||||
import KeyboardShortcuts from './components/misc/keyboard-shortcuts/index.vue'
|
||||
import TopNavigation from './components/home/topNavigation.vue'
|
||||
import ContentAuth from './components/home/contentAuth.vue'
|
||||
import ContentLinkShare from './components/home/contentLinkShare.vue'
|
||||
import ContentNoAuth from './components/home/contentNoAuth.vue'
|
||||
import Ready from '@/components/misc/ready.vue'
|
||||
|
||||
import Notification from './components/misc/notification'
|
||||
import {KEYBOARD_SHORTCUTS_ACTIVE, ONLINE} from './store/mutation-types'
|
||||
import KeyboardShortcuts from './components/misc/keyboard-shortcuts'
|
||||
import TopNavigation from './components/home/topNavigation'
|
||||
import ContentAuth from './components/home/contentAuth'
|
||||
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'
|
||||
import {ONLINE} from '@/store/mutation-types'
|
||||
|
||||
import {useColorScheme} from '@/composables/useColorScheme'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'app',
|
||||
components: {
|
||||
ContentNoAuth,
|
||||
ContentLinkShare,
|
||||
ContentAuth,
|
||||
TopNavigation,
|
||||
KeyboardShortcuts,
|
||||
Notification,
|
||||
Ready,
|
||||
},
|
||||
beforeMount() {
|
||||
this.setupOnlineStatus()
|
||||
},
|
||||
beforeCreate() {
|
||||
setLanguage()
|
||||
},
|
||||
setup() {
|
||||
useColorScheme()
|
||||
},
|
||||
created() {
|
||||
// Make sure to always load the home route when running with electron
|
||||
if (this.$route.fullPath.endsWith('frontend/index.html')) {
|
||||
this.$router.push({name: 'home'})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// Calling these methods in the mounted hook directly does not work.
|
||||
'$route.query.accountDeletionConfirm'() {
|
||||
this.setupAccountDeletionVerification()
|
||||
},
|
||||
'$route.query.userPasswordReset'() {
|
||||
this.setupPasswortResetRedirect()
|
||||
},
|
||||
'$route.query.userEmailConfirm'() {
|
||||
this.setupEmailVerificationRedirect()
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isTouch() {
|
||||
return isTouchDevice()
|
||||
},
|
||||
...mapState({
|
||||
online: ONLINE,
|
||||
keyboardShortcutsActive: KEYBOARD_SHORTCUTS_ACTIVE,
|
||||
}),
|
||||
...mapGetters('auth', [
|
||||
'authUser',
|
||||
'authLinkShare',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
setupOnlineStatus() {
|
||||
this.$store.commit(ONLINE, navigator.onLine)
|
||||
window.addEventListener('online', () => this.$store.commit(ONLINE, navigator.onLine))
|
||||
window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine))
|
||||
},
|
||||
setupPasswortResetRedirect() {
|
||||
if (typeof this.$route.query.userPasswordReset === 'undefined') {
|
||||
return
|
||||
}
|
||||
const store = useStore()
|
||||
const online = useOnline()
|
||||
watchEffect(() => store.commit(ONLINE, online.value))
|
||||
|
||||
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
|
||||
this.$router.push({name: 'user.password-reset.reset'})
|
||||
},
|
||||
setupEmailVerificationRedirect() {
|
||||
if (typeof this.$route.query.userEmailConfirm === 'undefined') {
|
||||
return
|
||||
}
|
||||
const router = useRouter()
|
||||
|
||||
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
|
||||
this.$router.push({name: 'user.login'})
|
||||
},
|
||||
async setupAccountDeletionVerification() {
|
||||
if (typeof this.$route.query.accountDeletionConfirm === 'undefined') {
|
||||
const isTouch = computed(isTouchDevice)
|
||||
const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive)
|
||||
|
||||
const authUser = computed(() => store.getters['auth/authUser'])
|
||||
const authLinkShare = computed(() => store.getters['auth/authLinkShare'])
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
// setup account deletion verification
|
||||
const accountDeletionConfirm = useRouteQuery('accountDeletionConfirm') as Ref<null | string>
|
||||
watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
||||
if (accountDeletionConfirm === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const accountDeletionService = new AccountDeleteService()
|
||||
await accountDeletionService.confirm(this.$route.query.accountDeletionConfirm)
|
||||
this.$message.success({message: this.$t('user.deletion.confirmSuccess')})
|
||||
this.$store.dispatch('auth/refreshUserInfo')
|
||||
},
|
||||
},
|
||||
})
|
||||
await accountDeletionService.confirm(accountDeletionConfirm)
|
||||
success({message: t('user.deletion.confirmSuccess')})
|
||||
store.dispatch('auth/refreshUserInfo')
|
||||
}, { immediate: true })
|
||||
|
||||
// setup passwort reset redirect
|
||||
const userPasswordReset = useRouteQuery('userPasswordReset') as Ref<null | string>
|
||||
watch(userPasswordReset, (userPasswordReset) => {
|
||||
if (userPasswordReset === null) {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('passwordResetToken', userPasswordReset)
|
||||
router.push({name: 'user.password-reset.reset'})
|
||||
}, { immediate: true })
|
||||
|
||||
// setup email verification redirect
|
||||
const userEmailConfirm = useRouteQuery('userEmailConfirm') as Ref<null | string>
|
||||
watch(userEmailConfirm, (userEmailConfirm) => {
|
||||
if (userEmailConfirm === null) {
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('emailConfirmToken', userEmailConfirm)
|
||||
router.push({name: 'user.login'})
|
||||
}, { immediate: true })
|
||||
|
||||
setLanguage()
|
||||
useColorScheme()
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -40,96 +40,88 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
<script lang="ts" setup>
|
||||
import {watch, computed} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useEventListener} from '@vueuse/core'
|
||||
|
||||
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
|
||||
import Navigation from '@/components/home/navigation.vue'
|
||||
import QuickActions from '@/components/quick-actions/quick-actions.vue'
|
||||
|
||||
export default {
|
||||
name: 'contentAuth',
|
||||
components: {QuickActions, Navigation},
|
||||
watch: {
|
||||
'$route': {
|
||||
handler: 'doStuffAfterRoute',
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.renewTokenOnFocus()
|
||||
this.loadLabels()
|
||||
},
|
||||
computed: mapState({
|
||||
background: 'background',
|
||||
menuActive: MENU_ACTIVE,
|
||||
userInfo: state => state.auth.info,
|
||||
authenticated: state => state.auth.authenticated,
|
||||
}),
|
||||
methods: {
|
||||
doStuffAfterRoute() {
|
||||
// this.setTitle('') // Reset the title if the page component does not set one itself
|
||||
this.hideMenuOnMobile()
|
||||
this.resetCurrentList()
|
||||
},
|
||||
resetCurrentList() {
|
||||
// Reset the current list highlight in menu if the current list is not list related.
|
||||
const store = useStore()
|
||||
|
||||
const background = computed(() => store.state.background)
|
||||
const menuActive = computed(() => store.state.menuActive)
|
||||
|
||||
function showKeyboardShortcuts() {
|
||||
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, true)
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// hide menu on mobile
|
||||
watch(() => route.fullPath, () => window.innerWidth < 769 && store.commit(MENU_ACTIVE, false))
|
||||
|
||||
// Reset the current list highlight in menu if the current route is not list related.
|
||||
watch(() => route.fullPath, () => {
|
||||
if (
|
||||
this.$route.name === 'home' ||
|
||||
this.$route.name === 'namespace.edit' ||
|
||||
this.$route.name === 'teams.index' ||
|
||||
this.$route.name === 'teams.edit' ||
|
||||
this.$route.name === 'tasks.range' ||
|
||||
this.$route.name === 'labels.index' ||
|
||||
this.$route.name === 'migrate.start' ||
|
||||
this.$route.name === 'migrate.wunderlist' ||
|
||||
this.$route.name.startsWith('user.settings') ||
|
||||
this.$route.name === 'namespaces.index'
|
||||
[
|
||||
'home',
|
||||
'namespace.edit',
|
||||
'teams.index',
|
||||
'teams.edit',
|
||||
'tasks.range',
|
||||
'labels.index',
|
||||
'migrate.start',
|
||||
'migrate.wunderlist',
|
||||
'namespaces.index',
|
||||
].includes(route.name) ||
|
||||
route.name.startsWith('user.settings')
|
||||
) {
|
||||
return this.$store.dispatch(CURRENT_LIST, null)
|
||||
store.dispatch(CURRENT_LIST, null)
|
||||
}
|
||||
},
|
||||
renewTokenOnFocus() {
|
||||
})
|
||||
|
||||
// TODO: Reset the title if the page component does not set one itself
|
||||
|
||||
function useRenewTokenOnFocus() {
|
||||
const router = useRouter()
|
||||
|
||||
const userInfo = computed(() => store.state.auth.info)
|
||||
const authenticated = computed(() => store.state.auth.authenticated)
|
||||
|
||||
// Try renewing the token every time vikunja is loaded initially
|
||||
// (When opening the browser the focus event is not fired)
|
||||
this.$store.dispatch('auth/renewToken')
|
||||
store.dispatch('auth/renewToken')
|
||||
|
||||
// Check if the token is still valid if the window gets focus again to maybe renew it
|
||||
window.addEventListener('focus', () => {
|
||||
|
||||
if (!this.authenticated) {
|
||||
useEventListener('focus', () => {
|
||||
if (!authenticated.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const expiresIn = (this.userInfo !== null ? this.userInfo.exp : 0) - +new Date() / 1000
|
||||
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - +new Date() / 1000
|
||||
|
||||
// If the token expiry is negative, it is already expired and we have no choice but to redirect
|
||||
// the user to the login page
|
||||
if (expiresIn < 0) {
|
||||
this.$store.dispatch('auth/checkAuth')
|
||||
this.$router.push({name: 'user.login'})
|
||||
store.dispatch('auth/checkAuth')
|
||||
router.push({name: 'user.login'})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the token is valid for less than 60 hours and renew if thats the case
|
||||
if (expiresIn < 60 * 3600) {
|
||||
this.$store.dispatch('auth/renewToken')
|
||||
store.dispatch('auth/renewToken')
|
||||
console.debug('renewed token')
|
||||
}
|
||||
})
|
||||
},
|
||||
hideMenuOnMobile() {
|
||||
if (window.innerWidth < 769) {
|
||||
this.$store.commit(MENU_ACTIVE, false)
|
||||
}
|
||||
},
|
||||
showKeyboardShortcuts() {
|
||||
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, true)
|
||||
},
|
||||
loadLabels() {
|
||||
this.$store.dispatch('labels/loadAllLabels')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
useRenewTokenOnFocus()
|
||||
store.dispatch('labels/loadAllLabels')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -21,23 +21,16 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'vuex'
|
||||
<script lang="ts" setup>
|
||||
import {computed} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
import PoweredByLink from './PoweredByLink.vue'
|
||||
|
||||
export default {
|
||||
name: 'contentLinkShare',
|
||||
components: {
|
||||
Logo,
|
||||
PoweredByLink,
|
||||
},
|
||||
computed: mapState([
|
||||
'currentList',
|
||||
'background',
|
||||
]),
|
||||
}
|
||||
const store = useStore()
|
||||
const currentList = computed(() => store.state.currentList)
|
||||
const background = computed(() => store.state.background)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -4,44 +4,38 @@
|
|||
</no-auth-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
<script lang="ts" setup>
|
||||
import {watchEffect} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
|
||||
|
||||
export default {
|
||||
name: 'contentNoAuth',
|
||||
components: {NoAuthWrapper},
|
||||
computed: {
|
||||
routeName() {
|
||||
return this.$route.name
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
routeName: {
|
||||
handler(routeName) {
|
||||
if (!routeName) return
|
||||
this.redirectToHome()
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
redirectToHome() {
|
||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
watchEffect(() => {
|
||||
if (!route.name) return
|
||||
redirectToHome()
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
function redirectToHome() {
|
||||
// Check if the user is already logged in and redirect them to the home page if not
|
||||
if (
|
||||
this.$route.name !== 'user.login' &&
|
||||
this.$route.name !== 'user.password-reset.request' &&
|
||||
this.$route.name !== 'user.password-reset.reset' &&
|
||||
this.$route.name !== 'user.register' &&
|
||||
this.$route.name !== 'link-share.auth' &&
|
||||
this.$route.name !== 'openid.auth' &&
|
||||
![
|
||||
'user.login',
|
||||
'user.password-reset.request',
|
||||
'user.password-reset.reset',
|
||||
'user.register',
|
||||
'link-share.auth',
|
||||
'openid.auth',
|
||||
].includes(route.name) &&
|
||||
localStorage.getItem('passwordResetToken') === null &&
|
||||
localStorage.getItem('emailConfirmToken') === null
|
||||
) {
|
||||
saveLastVisited(this.$route.name, this.$route.params)
|
||||
this.$router.push({name: 'user.login'})
|
||||
saveLastVisited(route.name, route.params)
|
||||
router.push({name: 'user.login'})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -28,7 +28,7 @@ export default class AbstractService {
|
|||
|
||||
/**
|
||||
* The abstract constructor.
|
||||
* @param paths An object with all paths. Default values are specified above.
|
||||
* @param [paths] An object with all paths. Default values are specified above.
|
||||
*/
|
||||
constructor(paths) {
|
||||
this.http = axios.create({
|
||||
|
|
|
@ -16,83 +16,101 @@
|
|||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||
v-model="password"
|
||||
v-focus
|
||||
@keyup.enter.prevent="auth"
|
||||
@keyup.enter.prevent="authenticate()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-button @click="auth" :loading="loading">
|
||||
<x-button @click="authenticate()" :loading="loading">
|
||||
{{ $t('user.auth.login') }}
|
||||
</x-button>
|
||||
|
||||
<message variant="danger" class="mt-4" v-if="errorMessage !== ''">
|
||||
<Message variant="danger" class="mt-4" v-if="errorMessage !== ''">
|
||||
{{ errorMessage }}
|
||||
</message>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapGetters} from 'vuex'
|
||||
import Message from '@/components/misc/message'
|
||||
<script lang="ts" setup>
|
||||
import {ref, computed} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useTitle} from '@vueuse/core'
|
||||
|
||||
export default {
|
||||
name: 'LinkSharingAuth',
|
||||
components: {Message},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
authenticateWithPassword: false,
|
||||
errorMessage: '',
|
||||
import Message from '@/components/misc/message.vue'
|
||||
|
||||
hash: '',
|
||||
password: '',
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.auth()
|
||||
},
|
||||
mounted() {
|
||||
this.setTitle(this.$t('sharing.authenticating'))
|
||||
},
|
||||
computed: mapGetters('auth', [
|
||||
'authLinkShare',
|
||||
]),
|
||||
methods: {
|
||||
async auth() {
|
||||
this.errorMessage = ''
|
||||
const {t} = useI18n()
|
||||
useTitle(t('sharing.authenticating'))
|
||||
|
||||
if (this.authLinkShare) {
|
||||
async function useAuth() {
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const authenticateWithPassword = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const password = ref('')
|
||||
|
||||
const authLinkShare = computed(() => store.getters['auth/authLinkShare'])
|
||||
|
||||
async function authenticate() {
|
||||
authenticateWithPassword.value = false
|
||||
errorMessage.value = ''
|
||||
|
||||
if (authLinkShare.value) {
|
||||
// FIXME: push to 'list.list' since authenticated?
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
// TODO: no password
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const r = await this.$store.dispatch('auth/linkShareAuth', {
|
||||
hash: this.$route.params.share,
|
||||
password: this.password,
|
||||
const {list_id: listId} = await store.dispatch('auth/linkShareAuth', {
|
||||
hash: route.params.share,
|
||||
password: password.value,
|
||||
})
|
||||
this.$router.push({name: 'list.list', params: {listId: r.list_id}})
|
||||
router.push({name: 'list.list', params: {listId}})
|
||||
} catch (e) {
|
||||
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) {
|
||||
this.authenticateWithPassword = true
|
||||
if (e.response?.data?.code === 13001) {
|
||||
authenticateWithPassword.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Put this logic in a global errorMessage handler method which checks all auth codes
|
||||
let errorMessage = this.$t('sharing.error')
|
||||
if (e.response && e.response.data && e.response.data.message) {
|
||||
let errorMessage = t('sharing.error')
|
||||
if (e.response?.data?.message) {
|
||||
errorMessage = e.response.data.message
|
||||
}
|
||||
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13002) {
|
||||
errorMessage = this.$t('sharing.invalidPassword')
|
||||
if (e.response?.data?.code === 13002) {
|
||||
errorMessage = t('sharing.invalidPassword')
|
||||
}
|
||||
this.errorMessage = errorMessage
|
||||
errorMessage.value = errorMessage
|
||||
} finally {
|
||||
this.loading = false
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
authenticate()
|
||||
|
||||
return {
|
||||
loading,
|
||||
authenticateWithPassword,
|
||||
errorMessage,
|
||||
password,
|
||||
authenticate,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const {
|
||||
loading,
|
||||
authenticateWithPassword,
|
||||
errorMessage,
|
||||
password,
|
||||
authenticate,
|
||||
} = useAuth()
|
||||
</script>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import ShowTasks from './ShowTasks'
|
||||
|
||||
|
|
|
@ -3787,6 +3787,14 @@
|
|||
"@vueuse/shared" "7.3.0"
|
||||
vue-demi "*"
|
||||
|
||||
"@vueuse/router@^7.3.0":
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-7.3.0.tgz#8157d20636040e573379eda338b57d47f1464163"
|
||||
integrity sha512-MRGaZPVV21MZOZ759LMRWTlSaRvcKh+kGw2tGCLhxkihObcrNNLl0h3N5QtBy/+eR84tf7MO7JHCL+0PJspzQg==
|
||||
dependencies:
|
||||
"@vueuse/shared" "7.3.0"
|
||||
vue-demi "*"
|
||||
|
||||
"@vueuse/shared@7.3.0":
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-7.3.0.tgz#729b2f0a83f38647896d955902e828dcbd8ed7dc"
|
||||
|
|
Loading…
Reference in a new issue