Merge remote-tracking branch 'upstream/main' into set-all-variables
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
Adrian Simmons 2021-12-14 14:17:04 +00:00
commit 3a85bf9b23
40 changed files with 1369 additions and 1470 deletions

View File

@ -202,7 +202,6 @@ steps:
YARN_CACHE_FOLDER: .cache/yarn/ YARN_CACHE_FOLDER: .cache/yarn/
commands: commands:
- yarn --frozen-lockfile --network-timeout 100000 - yarn --frozen-lockfile --network-timeout 100000
- npx browserslist@latest --update-db
- yarn run lint - yarn run lint
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json" - "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- yarn run build - yarn run build

View File

@ -19,10 +19,11 @@
"dependencies": { "dependencies": {
"@github/hotkey": "1.6.0", "@github/hotkey": "1.6.0",
"@kyvg/vue3-notification": "2.3.4", "@kyvg/vue3-notification": "2.3.4",
"@sentry/tracing": "6.16.0", "@sentry/tracing": "6.16.1",
"@sentry/vue": "6.16.0", "@sentry/vue": "6.16.1",
"@vue/compat": "3.2.24", "@vue/compat": "3.2.26",
"@vueuse/core": "7.2.2", "@vueuse/core": "7.3.0",
"@vueuse/router": "7.3.0",
"bulma-css-variables": "0.9.33", "bulma-css-variables": "0.9.33",
"camel-case": "4.1.2", "camel-case": "4.1.2",
"codemirror": "5.64.0", "codemirror": "5.64.0",
@ -36,12 +37,12 @@
"is-touch-device": "1.0.1", "is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0", "lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"marked": "4.0.6", "marked": "4.0.7",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
"snake-case": "3.0.4", "snake-case": "3.0.4",
"ufo": "0.7.9", "ufo": "0.7.9",
"v-tooltip": "4.0.0-beta.2", "v-tooltip": "4.0.0-beta.2",
"vue": "3.2.24", "vue": "3.2.26",
"vue-advanced-cropper": "2.7.0", "vue-advanced-cropper": "2.7.0",
"vue-drag-resize": "2.0.3", "vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.5", "vue-flatpickr-component": "9.0.5",
@ -59,34 +60,35 @@
"@fortawesome/vue-fontawesome": "3.0.0-5", "@fortawesome/vue-fontawesome": "3.0.0-5",
"@types/flexsearch": "0.7.2", "@types/flexsearch": "0.7.2",
"@types/jest": "27.0.3", "@types/jest": "27.0.3",
"@typescript-eslint/eslint-plugin": "5.6.0", "@typescript-eslint/eslint-plugin": "5.7.0",
"@typescript-eslint/parser": "5.6.0", "@typescript-eslint/parser": "5.7.0",
"@vitejs/plugin-legacy": "1.6.4", "@vitejs/plugin-legacy": "1.6.4",
"@vitejs/plugin-vue": "1.10.2", "@vitejs/plugin-vue": "2.0.0",
"@vue/eslint-config-typescript": "9.1.0", "@vue/eslint-config-typescript": "9.1.0",
"autoprefixer": "10.4.0", "autoprefixer": "10.4.0",
"axios": "0.24.0", "axios": "0.24.0",
"browserslist": "4.18.1", "browserslist": "4.19.0",
"cypress": "8.7.0", "caniuse-lite": "1.0.30001286",
"cypress": "9.1.1",
"cypress-file-upload": "5.0.8", "cypress-file-upload": "5.0.8",
"esbuild": "0.14.2", "esbuild": "0.14.3",
"eslint": "8.4.1", "eslint": "8.4.1",
"eslint-plugin-vue": "8.2.0", "eslint-plugin-vue": "8.2.0",
"express": "4.17.1", "express": "4.17.1",
"faker": "5.5.3", "faker": "5.5.3",
"jest": "27.4.3", "jest": "27.4.5",
"netlify-cli": "8.0.16", "netlify-cli": "8.1.1",
"postcss": "8.4.4", "postcss": "8.4.5",
"postcss-preset-env": "7.0.1", "postcss-preset-env": "7.0.1",
"rollup": "2.60.2", "rollup": "2.61.1",
"rollup-plugin-visualizer": "5.5.2", "rollup-plugin-visualizer": "5.5.2",
"sass": "1.44.0", "sass": "1.45.0",
"slugify": "1.6.3", "slugify": "1.6.3",
"ts-jest": "27.1.1", "ts-jest": "27.1.1",
"typescript": "4.5.2", "typescript": "4.5.4",
"vite": "2.7.1", "vite": "2.7.2",
"vite-plugin-pwa": "0.11.10", "vite-plugin-pwa": "0.11.11",
"vite-svg-loader": "3.1.0", "vite-svg-loader": "3.1.1",
"vue-tsc": "0.29.8", "vue-tsc": "0.29.8",
"wait-on": "6.0.0", "wait-on": "6.0.0",
"workbox-cli": "6.4.2" "workbox-cli": "6.4.2"

View File

@ -1,116 +1,96 @@
<template> <template>
<ready> <ready :class="{'is-touch': isTouch}">
<div :class="{'is-touch': isTouch}"> <div :class="{'is-hidden': !online}">
<div :class="{'is-hidden': !online}"> <template v-if="authUser">
<template v-if="authUser"> <top-navigation/>
<top-navigation/> <content-auth/>
<content-auth/> </template>
</template> <content-link-share v-else-if="authLinkShare"/>
<content-link-share v-else-if="authLinkShare"/> <content-no-auth v-else/>
<content-no-auth v-else/> <notification/>
<notification/>
</div>
<transition name="fade">
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
</transition>
</div> </div>
<transition name="fade">
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
</transition>
</ready> </ready>
</template> </template>
<script> <script lang="ts" setup>
import {defineComponent} from 'vue' import {computed, watch, watchEffect, Ref} from 'vue'
import {mapState, mapGetters} from 'vuex' 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 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 {setLanguage} from './i18n'
import AccountDeleteService from '@/services/accountDelete' import AccountDeleteService from '@/services/accountDelete'
import Ready from '@/components/misc/ready' import {ONLINE} from '@/store/mutation-types'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
export default defineComponent({ const store = useStore()
name: 'app', const online = useOnline()
components: { watchEffect(() => store.commit(ONLINE, online.value))
ContentNoAuth,
ContentLinkShare,
ContentAuth,
TopNavigation,
KeyboardShortcuts,
Notification,
Ready,
},
beforeMount() {
this.setupOnlineStatus()
this.setupPasswortResetRedirect()
this.setupEmailVerificationRedirect()
this.setupAccountDeletionVerification()
},
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'})
}
},
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
}
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset) const router = useRouter()
this.$router.push({name: 'user.password-reset.reset'})
},
setupEmailVerificationRedirect() {
if (typeof this.$route.query.userEmailConfirm === 'undefined') {
return
}
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm) const isTouch = computed(isTouchDevice)
this.$router.push({name: 'user.login'}) const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive)
},
async setupAccountDeletionVerification() {
if (typeof this.$route.query.accountDeletionConfirm === 'undefined') {
return
}
const accountDeletionService = new AccountDeleteService() const authUser = computed(() => store.getters['auth/authUser'])
await accountDeletionService.confirm(this.$route.query.accountDeletionConfirm) const authLinkShare = computed(() => store.getters['auth/authLinkShare'])
this.$message.success({message: this.$t('user.deletion.confirmSuccess')})
this.$store.dispatch('auth/refreshUserInfo') 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(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> </script>
<style lang="scss"> <style lang="scss">

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

View File

@ -40,96 +40,88 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import {mapState} from 'vuex' 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 {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
import Navigation from '@/components/home/navigation.vue' import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue' import QuickActions from '@/components/quick-actions/quick-actions.vue'
export default { const store = useStore()
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.
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'
) {
return this.$store.dispatch(CURRENT_LIST, null)
}
},
renewTokenOnFocus() {
// 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')
// Check if the token is still valid if the window gets focus again to maybe renew it const background = computed(() => store.state.background)
window.addEventListener('focus', () => { const menuActive = computed(() => store.state.menuActive)
if (!this.authenticated) { function showKeyboardShortcuts() {
return store.commit(KEYBOARD_SHORTCUTS_ACTIVE, true)
}
const expiresIn = (this.userInfo !== null ? this.userInfo.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'})
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')
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')
},
},
} }
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 (
[
'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')
) {
store.dispatch(CURRENT_LIST, null)
}
})
// 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)
store.dispatch('auth/renewToken')
// Check if the token is still valid if the window gets focus again to maybe renew it
useEventListener('focus', () => {
if (!authenticated.value) {
return
}
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) {
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) {
store.dispatch('auth/renewToken')
console.debug('renewed token')
}
})
}
useRenewTokenOnFocus()
store.dispatch('labels/loadAllLabels')
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -21,23 +21,16 @@
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import {mapState} from 'vuex' import {computed} from 'vue'
import {useStore} from 'vuex'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue' import PoweredByLink from './PoweredByLink.vue'
export default { const store = useStore()
name: 'contentLinkShare', const currentList = computed(() => store.state.currentList)
components: { const background = computed(() => store.state.background)
Logo,
PoweredByLink,
},
computed: mapState([
'currentList',
'background',
]),
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,44 +4,38 @@
</no-auth-wrapper> </no-auth-wrapper>
</template> </template>
<script> <script lang="ts" setup>
import {saveLastVisited} from '@/helpers/saveLastVisited' import {watchEffect} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper' import NoAuthWrapper from '@/components/misc/no-auth-wrapper'
export default { import {saveLastVisited} from '@/helpers/saveLastVisited'
name: 'contentNoAuth',
components: {NoAuthWrapper}, const route = useRoute()
computed: {
routeName() { watchEffect(() => {
return this.$route.name if (!route.name) return
}, redirectToHome()
}, })
watch: {
routeName: { const router = useRouter()
handler(routeName) { function redirectToHome() {
if (!routeName) return // Check if the user is already logged in and redirect them to the home page if not
this.redirectToHome() if (
}, ![
immediate: true, 'user.login',
}, 'user.password-reset.request',
}, 'user.password-reset.reset',
methods: { 'user.register',
redirectToHome() { 'link-share.auth',
// Check if the user is already logged in and redirect them to the home page if not 'openid.auth',
if ( ].includes(route.name) &&
this.$route.name !== 'user.login' && localStorage.getItem('passwordResetToken') === null &&
this.$route.name !== 'user.password-reset.request' && localStorage.getItem('emailConfirmToken') === null
this.$route.name !== 'user.password-reset.reset' && ) {
this.$route.name !== 'user.register' && saveLastVisited(route.name, route.params)
this.$route.name !== 'link-share.auth' && router.push({name: 'user.login'})
this.$route.name !== 'openid.auth' && }
localStorage.getItem('passwordResetToken') === null &&
localStorage.getItem('emailConfirmToken') === null
) {
saveLastVisited(this.$route.name, this.$route.params)
this.$router.push({name: 'user.login'})
}
},
},
} }
</script> </script>

View File

@ -457,7 +457,7 @@ export default {
text-transform: none; text-transform: none;
font-family: $family-sans-serif; font-family: $family-sans-serif;
font-weight: normal; font-weight: normal;
padding: .5rem 0; padding: .5rem;
border: none; border: none;
cursor: pointer; cursor: pointer;
@ -477,7 +477,7 @@ export default {
font-size: .75rem; font-size: .75rem;
color: transparent; color: transparent;
transition: color $transition; transition: color $transition;
padding: 0 .5rem; padding-left: .5rem;
} }
&:focus, &:hover { &:focus, &:hover {

View File

@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<div class="api-url-info" v-else> <div class="api-url-info" v-else>
<i18n-t keypath="apiConfig.signInOn"> <i18n-t keypath="apiConfig.use">
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span> <span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
</i18n-t> </i18n-t>
<br/> <br/>
@ -101,7 +101,7 @@ export default {
// Set it + save it to local storage to save us the hoops // Set it + save it to local storage to save us the hoops
this.errorMsg = '' this.errorMsg = ''
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain}) this.$message.success({message: this.$t('apiConfig.success', {domain: this.apiDomain})})
this.configureApi = false this.configureApi = false
this.apiUrl = url this.apiUrl = url
this.$emit('foundApi', this.apiUrl) this.$emit('foundApi', this.apiUrl)

View File

@ -1,6 +1,8 @@
<template> <template>
<div class="message" :class="variant"> <div class="message-wrapper">
<slot/> <div class="message" :class="variant">
<slot/>
</div>
</div> </div>
</template> </template>
@ -14,6 +16,11 @@ defineProps({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.message-wrapper {
border-radius: $radius;
background: var(--white);
}
.message { .message {
padding: .75rem 1rem; padding: .75rem 1rem;
border-radius: $radius; border-radius: $radius;

View File

@ -1,40 +1,134 @@
<template> <template>
<div class="no-auth-wrapper"> <div class="no-auth-wrapper">
<Logo class="logo" width="200" height="58"/>
<div class="noauth-container"> <div class="noauth-container">
<Logo class="logo" width="400" height="117" /> <section class="image" :class="{'has-message': motd !== ''}">
<Message v-if="motd !== ''" class="my-2"> <Message v-if="motd !== ''">
{{ motd }} {{ motd }}
</Message> </Message>
<slot/> <h2 class="image-title">
{{ $t('misc.welcomeBack') }}
</h2>
</section>
<section class="content">
<div>
<h2 class="title" v-if="title">{{ title }}</h2>
<api-config @foundApi="hasApiUrl = true"/>
<slot/>
</div>
<legal/>
</section>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message'
import Legal from '@/components/misc/legal'
import ApiConfig from '@/components/misc/api-config.vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {computed} from 'vue' import {computed} from 'vue'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {useTitle} from '@/composables/useTitle'
const route = useRoute()
const store = useStore() const store = useStore()
const {t} = useI18n()
const motd = computed(() => store.state.config.motd) const motd = computed(() => store.state.config.motd)
// @ts-ignore
const title = computed(() => t(route.meta.title ?? ''))
useTitle(() => title.value)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.no-auth-wrapper { .no-auth-wrapper {
background: url('@/assets/llama.svg?url') no-repeat bottom left fixed var(--site-background); background: var(--site-background) url('@/assets/llama.svg?url') no-repeat fixed bottom left;
min-height: 100vh; min-height: 100vh;
display: flex;
flex-direction: column;
place-items: center;
@media screen and (max-width: $fullhd) {
padding-bottom: 15rem;
}
} }
.noauth-container { .noauth-container {
max-width: 450px; max-width: $desktop;
width: 100%; width: 100%;
margin: 0 auto; min-height: 60vh;
display: flex;
background-color: var(--white);
box-shadow: var(--shadow-md);
overflow: hidden;
@media screen and (min-width: $desktop) {
border-radius: $radius;
}
}
.image {
width: 50%;
padding: 1rem; padding: 1rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
@media screen and (max-width: $tablet) {
display: none;
}
@media screen and (min-width: $tablet) {
background: url('@/assets/no-auth-image.jpg') no-repeat bottom/cover;
position: relative;
&.has-message {
justify-content: space-between;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, .2);
}
> * {
position: relative;
}
}
}
.content {
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 2rem 2rem 1.5rem;
@media screen and (max-width: $desktop) {
width: 100%;
max-width: 450px;
margin-inline: auto;
}
@media screen and (min-width: $desktop) {
width: 50%;
}
} }
.logo { .logo {
color: var(--logo-text-color);
max-width: 100%; max-width: 100%;
margin: 1rem 0;
}
.image-title {
color: var(--white);
font-size: 2.5rem;
} }
</style> </style>

View File

@ -21,11 +21,11 @@
<li :key="`page-${i}`" v-for="(p, i) in pages"> <li :key="`page-${i}`" v-for="(p, i) in pages">
<span class="pagination-ellipsis" v-if="p.isEllipsis">&hellip;</span> <span class="pagination-ellipsis" v-if="p.isEllipsis">&hellip;</span>
<router-link <router-link
v-else
class="pagination-link"
:aria-label="'Goto page ' + p.number" :aria-label="'Goto page ' + p.number"
:class="{ 'is-current': p.number === currentPage }" :class="{ 'is-current': p.number === currentPage }"
:to="getRouteForPagination(p.number)" :to="getRouteForPagination(p.number)"
class="pagination-link"
v-else
> >
{{ p.number }} {{ p.number }}
</router-link> </router-link>
@ -98,13 +98,13 @@ const pages = computed(() => createPagination(props.totalPages, props.currentPag
<style lang="scss" scoped> <style lang="scss" scoped>
.pagination { .pagination {
padding-bottom: 1rem; padding-bottom: 1rem;
}
.pagination-previous, .pagination-previous,
.pagination-next { .pagination-next {
&:not(:disabled):hover { &:not(:disabled):hover {
background: $scheme-main; background: $scheme-main;
cursor: pointer; cursor: pointer;
}
} }
} }
</style> </style>

View File

@ -16,60 +16,54 @@
</multiselect> </multiselect>
</template> </template>
<script> <script lang="ts" setup>
import ListModel from '../../../models/list' import {reactive, ref, watchEffect} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n'
import ListModel from '@/models/list'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
export default { const store = useStore()
name: 'listSearch', const {t} = useI18n()
data() {
return {
list: new ListModel(),
foundLists: [],
}
},
props: {
modelValue: {
required: false,
},
},
emits: ['update:modelValue', 'selected'],
components: {
Multiselect,
},
watch: {
modelValue: {
handler(value) {
this.list = value
},
immeditate: true,
deep: true,
},
},
methods: {
findLists(query) {
this.foundLists = this.$store.getters['lists/searchList'](query)
},
select(list) { const list = reactive(new ListModel())
this.list = list const props = defineProps({
this.$emit('selected', list) modelValue: {
this.$emit('update:modelValue', list) validator(value) {
}, return value instanceof ListModel
namespace(namespaceId) {
const namespace = this.$store.getters['namespaces/getNamespaceById'](namespaceId)
if (namespace !== null) {
return namespace.title
}
return this.$t('list.shared')
}, },
required: false,
}, },
})
const emit = defineEmits(['update:modelValue'])
watchEffect(() => {
Object.assign(list, props.modelValue)
})
const foundLists = ref([])
function findLists(query: string) {
if (query === '') {
select(null)
}
foundLists.value = store.getters['lists/searchList'](query)
}
function select(l: ListModel | null) {
Object.assign(list, l)
emit('update:modelValue', list)
}
function namespace(namespaceId: number) {
const namespace = store.getters['namespaces/getNamespaceById'](namespaceId)
return namespace !== null
? namespace.title
: t('list.shared')
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.list-namespace-title { .list-namespace-title {
color: var(--grey-500); color: var(--grey-500);
} }
</style> </style>

View File

@ -36,6 +36,7 @@
"password": "Heslo", "password": "Heslo",
"passwordRepeat": "Zopakovat heslo", "passwordRepeat": "Zopakovat heslo",
"passwordPlaceholder": "např. • • • • • • • •", "passwordPlaceholder": "např. • • • • • • • •",
"forgotPassword": "Forgot your password?",
"resetPassword": "Obnovit heslo", "resetPassword": "Obnovit heslo",
"resetPasswordAction": "Poslat odkaz na obnovení hesla", "resetPasswordAction": "Poslat odkaz na obnovení hesla",
"resetPasswordSuccess": "Zkontrolujte doručenou poštu! Měli byste mít e-mail s pokyny, jak obnovit své heslo.", "resetPasswordSuccess": "Zkontrolujte doručenou poštu! Měli byste mít e-mail s pokyny, jak obnovit své heslo.",
@ -473,7 +474,8 @@
"download": "Stáhnout", "download": "Stáhnout",
"showMenu": "Zobrazit nabídku", "showMenu": "Zobrazit nabídku",
"hideMenu": "Skrýt nabídku", "hideMenu": "Skrýt nabídku",
"forExample": "Například:" "forExample": "Například:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Obnovit barvu", "resetColor": "Obnovit barvu",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "např. https://localhost:3456", "urlPlaceholder": "např. https://localhost:3456",
"change": "změnit", "change": "změnit",
"signInOn": "Přihlaste se ke svému účtu Vikunja na {0}", "use": "Using Vikunja installation at {0}",
"error": "Nelze najít nebo použít instalaci Vikunja na \"{domain}\". Zkuste prosím jinou url.", "error": "Nelze najít nebo použít instalaci Vikunja na \"{domain}\". Zkuste prosím jinou url.",
"success": "Pomocí instalace Vikunja na \"{domain}\".", "success": "Pomocí instalace Vikunja na \"{domain}\".",
"urlRequired": "Je vyžadována adresa URL." "urlRequired": "Je vyžadována adresa URL."

View File

@ -36,6 +36,7 @@
"password": "Passwort", "password": "Passwort",
"passwordRepeat": "Gib dein Passwort erneut ein", "passwordRepeat": "Gib dein Passwort erneut ein",
"passwordPlaceholder": "z.B. •••••••••••", "passwordPlaceholder": "z.B. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Setze dein Passwort zurück", "resetPassword": "Setze dein Passwort zurück",
"resetPasswordAction": "Sende mir einen Link zum Zurücksetzen des Passworts", "resetPasswordAction": "Sende mir einen Link zum Zurücksetzen des Passworts",
"resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.", "resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.",
@ -473,7 +474,8 @@
"download": "Herunterladen", "download": "Herunterladen",
"showMenu": "Menü anzeigen", "showMenu": "Menü anzeigen",
"hideMenu": "Menü ausblenden", "hideMenu": "Menü ausblenden",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Farbe zurücksetzen", "resetColor": "Farbe zurücksetzen",
@ -811,7 +813,7 @@
"url": "Vikunja-URL", "url": "Vikunja-URL",
"urlPlaceholder": "z.B. https://localhost:3456", "urlPlaceholder": "z.B. https://localhost:3456",
"change": "ändern", "change": "ändern",
"signInOn": "Melde dich bei deinem Vikunja-Account auf {0} an", "use": "Using Vikunja installation at {0}",
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.", "error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
"success": "Verwende die Vikunja-Installation unter „{domain}“.", "success": "Verwende die Vikunja-Installation unter „{domain}“.",
"urlRequired": "Eine Url ist erforderlich." "urlRequired": "Eine Url ist erforderlich."

View File

@ -36,6 +36,7 @@
"password": "Passwort", "password": "Passwort",
"passwordRepeat": "Gib dis Passwort nomal iih", "passwordRepeat": "Gib dis Passwort nomal iih",
"passwordPlaceholder": "z.B. •••••••••••", "passwordPlaceholder": "z.B. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Setz diis Passwort zrugg", "resetPassword": "Setz diis Passwort zrugg",
"resetPasswordAction": "Schick mir en Passwort zruggsetz Link", "resetPasswordAction": "Schick mir en Passwort zruggsetz Link",
"resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.", "resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.",
@ -473,7 +474,8 @@
"download": "Herunterladen", "download": "Herunterladen",
"showMenu": "Menü anzeigen", "showMenu": "Menü anzeigen",
"hideMenu": "Menü ausblenden", "hideMenu": "Menü ausblenden",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Farb zruggsetze", "resetColor": "Farb zruggsetze",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "z.B. https://localhost:3456", "urlPlaceholder": "z.B. https://localhost:3456",
"change": "ändere", "change": "ändere",
"signInOn": "Dich i diin Vikunja-Account Iihloge uf {0}", "use": "Using Vikunja installation at {0}",
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.", "error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
"success": "Benutze d'Vikunja Installation uf \"{domain}\".", "success": "Benutze d'Vikunja Installation uf \"{domain}\".",
"urlRequired": "Eine Url ist erforderlich." "urlRequired": "Eine Url ist erforderlich."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Mot de passe", "password": "Mot de passe",
"passwordRepeat": "Retape ton mot de passe", "passwordRepeat": "Retape ton mot de passe",
"passwordPlaceholder": "p. ex. •••••••••••", "passwordPlaceholder": "p. ex. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Réinitialiser ton mot de passe", "resetPassword": "Réinitialiser ton mot de passe",
"resetPasswordAction": "Menvoyer un lien de réinitialisation du mot de passe", "resetPasswordAction": "Menvoyer un lien de réinitialisation du mot de passe",
"resetPasswordSuccess": "Vérifie ta boîte de réception ! Tu devrais avoir un courriel contenant les instructions sur la manière de réinitialiser ton mot de passe.", "resetPasswordSuccess": "Vérifie ta boîte de réception ! Tu devrais avoir un courriel contenant les instructions sur la manière de réinitialiser ton mot de passe.",
@ -473,7 +474,8 @@
"download": "Télécharger", "download": "Télécharger",
"showMenu": "Afficher le menu", "showMenu": "Afficher le menu",
"hideMenu": "Masquer le menu", "hideMenu": "Masquer le menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Réinitialiser la couleur", "resetColor": "Réinitialiser la couleur",
@ -811,7 +813,7 @@
"url": "URL Vikunja", "url": "URL Vikunja",
"urlPlaceholder": "Par exemple : https://localhost:3456", "urlPlaceholder": "Par exemple : https://localhost:3456",
"change": "changer", "change": "changer",
"signInOn": "Se connecter à ton compte Vikunja sur {0}", "use": "Using Vikunja installation at {0}",
"error": "Impossible de trouver ou d'utiliser l'installation de Vikunja sur « {domain} ». Veuillez essayer une autre URL.", "error": "Impossible de trouver ou d'utiliser l'installation de Vikunja sur « {domain} ». Veuillez essayer une autre URL.",
"success": "Utilisation de linstallation Vikunja à « {domain} ».", "success": "Utilisation de linstallation Vikunja à « {domain} ».",
"urlRequired": "Une URL est requise." "urlRequired": "Une URL est requise."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Digita di nuovo la tua password", "passwordRepeat": "Digita di nuovo la tua password",
"passwordPlaceholder": "es. ••••••••••••", "passwordPlaceholder": "es. ••••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reimposta la tua password", "resetPassword": "Reimposta la tua password",
"resetPasswordAction": "Inviami il link per reimpostare la password", "resetPasswordAction": "Inviami il link per reimpostare la password",
"resetPasswordSuccess": "Controlla la tua casella di posta! Dovresti avere un'e-mail con le istruzioni su come reimpostare la password.", "resetPasswordSuccess": "Controlla la tua casella di posta! Dovresti avere un'e-mail con le istruzioni su come reimpostare la password.",
@ -473,7 +474,8 @@
"download": "Scarica", "download": "Scarica",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Ripristina Colore", "resetColor": "Ripristina Colore",
@ -811,7 +813,7 @@
"url": "URL Vikunja", "url": "URL Vikunja",
"urlPlaceholder": "es. http://localhost:8080", "urlPlaceholder": "es. http://localhost:8080",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Пароль", "password": "Пароль",
"passwordRepeat": "Пароль ещё раз", "passwordRepeat": "Пароль ещё раз",
"passwordPlaceholder": "напр. •••••••••••", "passwordPlaceholder": "напр. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Сбросить пароль", "resetPassword": "Сбросить пароль",
"resetPasswordAction": "Отправить ссылку на сброс пароля", "resetPasswordAction": "Отправить ссылку на сброс пароля",
"resetPasswordSuccess": "Проверь свою почту! Там должно быть письмо с инструкциями, как сбросить пароль.", "resetPasswordSuccess": "Проверь свою почту! Там должно быть письмо с инструкциями, как сбросить пароль.",
@ -473,7 +474,8 @@
"download": "Скачать", "download": "Скачать",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Сбросить цвет", "resetColor": "Сбросить цвет",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "напр. https://localhost:3456", "urlPlaceholder": "напр. https://localhost:3456",
"change": "изменить", "change": "изменить",
"signInOn": "Войди в свой аккаунт Vikunja на {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Используется Vikunja на \"{domain}\".", "success": "Используется Vikunja на \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View File

@ -36,6 +36,7 @@
"password": "Mật khẩu", "password": "Mật khẩu",
"passwordRepeat": "Nhập lại mật khẩu", "passwordRepeat": "Nhập lại mật khẩu",
"passwordPlaceholder": "ví dụ: •••••••••••", "passwordPlaceholder": "ví dụ: •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset mật khẩu của bạn", "resetPassword": "Reset mật khẩu của bạn",
"resetPasswordAction": "Gửi cho tôi liên kết reset mật khẩu", "resetPasswordAction": "Gửi cho tôi liên kết reset mật khẩu",
"resetPasswordSuccess": "Kiểm tra hộp thư của bạn! Bạn sẽ nhận một e-mail với hướng dẫn reset mật khẩu của mình.", "resetPasswordSuccess": "Kiểm tra hộp thư của bạn! Bạn sẽ nhận một e-mail với hướng dẫn reset mật khẩu của mình.",
@ -473,7 +474,8 @@
"download": "Tải về", "download": "Tải về",
"showMenu": "Hiển thị menu", "showMenu": "Hiển thị menu",
"hideMenu": "Ẩn menu", "hideMenu": "Ẩn menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Đặt lại màu", "resetColor": "Đặt lại màu",
@ -811,7 +813,7 @@
"url": "URL Vikunja", "url": "URL Vikunja",
"urlPlaceholder": "ví dụ: https://localhost:3456", "urlPlaceholder": "ví dụ: https://localhost:3456",
"change": "thay đổi", "change": "thay đổi",
"signInOn": "Đăng nhập vào tài khoản Vikunia tại {0}", "use": "Using Vikunja installation at {0}",
"error": "Không thể tìm thấy hoặc sử dụng cài đặt Vikunja tại \"{domain}\". Vui lòng thử một url khác.", "error": "Không thể tìm thấy hoặc sử dụng cài đặt Vikunja tại \"{domain}\". Vui lòng thử một url khác.",
"success": "Sử dụng cài đặt Vikunja tại \"{domain}\".", "success": "Sử dụng cài đặt Vikunja tại \"{domain}\".",
"urlRequired": "Cần có một url." "urlRequired": "Cần có một url."

View File

@ -105,21 +105,33 @@ const router = createRouter({
path: '/login', path: '/login',
name: 'user.login', name: 'user.login',
component: LoginComponent, component: LoginComponent,
meta: {
title: 'user.auth.login',
},
}, },
{ {
path: '/get-password-reset', path: '/get-password-reset',
name: 'user.password-reset.request', name: 'user.password-reset.request',
component: GetPasswordResetComponent, component: GetPasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
}, },
{ {
path: '/password-reset', path: '/password-reset',
name: 'user.password-reset.reset', name: 'user.password-reset.reset',
component: PasswordResetComponent, component: PasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
}, },
{ {
path: '/register', path: '/register',
name: 'user.register', name: 'user.register',
component: RegisterComponent, component: RegisterComponent,
meta: {
title: 'user.auth.register',
},
}, },
{ {
path: '/user/settings', path: '/user/settings',

View File

@ -28,7 +28,7 @@ export default class AbstractService {
/** /**
* The abstract constructor. * 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) { constructor(paths) {
this.http = axios.create({ this.http = axios.create({

View File

@ -3,7 +3,7 @@
<h2 v-if="userInfo"> <h2 v-if="userInfo">
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}! {{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
</h2> </h2>
<message variant="danger" v-if="deletionScheduledAt !== null"> <message variant="danger" v-if="deletionScheduledAt !== null" class="mb-4">
{{ {{
$t('user.deletion.scheduled', { $t('user.deletion.scheduled', {
date: formatDateShort(deletionScheduledAt), date: formatDateShort(deletionScheduledAt),

View File

@ -16,83 +16,101 @@
:placeholder="$t('user.auth.passwordPlaceholder')" :placeholder="$t('user.auth.passwordPlaceholder')"
v-model="password" v-model="password"
v-focus v-focus
@keyup.enter.prevent="auth" @keyup.enter.prevent="authenticate()"
/> />
</div> </div>
</div> </div>
<x-button @click="auth" :loading="loading"> <x-button @click="authenticate()" :loading="loading">
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
</x-button> </x-button>
<message variant="danger" class="mt-4" v-if="errorMessage !== ''"> <Message variant="danger" class="mt-4" v-if="errorMessage !== ''">
{{ errorMessage }} {{ errorMessage }}
</message> </Message>
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
import {mapGetters} from 'vuex' import {ref, computed} from 'vue'
import Message from '@/components/misc/message' import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {useTitle} from '@vueuse/core'
export default { import Message from '@/components/misc/message.vue'
name: 'LinkSharingAuth',
components: {Message},
data() {
return {
loading: true,
authenticateWithPassword: false,
errorMessage: '',
hash: '', const {t} = useI18n()
password: '', useTitle(t('sharing.authenticating'))
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
} }
},
created() { // TODO: no password
this.auth()
}, loading.value = true
mounted() {
this.setTitle(this.$t('sharing.authenticating'))
},
computed: mapGetters('auth', [
'authLinkShare',
]),
methods: {
async auth() {
this.errorMessage = ''
if (this.authLinkShare) { try {
const {list_id: listId} = await store.dispatch('auth/linkShareAuth', {
hash: route.params.share,
password: password.value,
})
router.push({name: 'list.list', params: {listId}})
} catch (e) {
if (e.response?.data?.code === 13001) {
authenticateWithPassword.value = true
return return
} }
this.loading = true // TODO: Put this logic in a global errorMessage handler method which checks all auth codes
let errorMessage = t('sharing.error')
try { if (e.response?.data?.message) {
const r = await this.$store.dispatch('auth/linkShareAuth', { errorMessage = e.response.data.message
hash: this.$route.params.share,
password: this.password,
})
this.$router.push({name: 'list.list', params: {listId: r.list_id}})
} catch (e) {
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13001) {
this.authenticateWithPassword = 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) {
errorMessage = e.response.data.message
}
if (typeof e.response.data.code !== 'undefined' && e.response.data.code === 13002) {
errorMessage = this.$t('sharing.invalidPassword')
}
this.errorMessage = errorMessage
} finally {
this.loading = false
} }
}, if (e.response?.data?.code === 13002) {
}, errorMessage = t('sharing.invalidPassword')
}
errorMessage.value = errorMessage
} finally {
loading.value = false
}
}
authenticate()
return {
loading,
authenticateWithPassword,
errorMessage,
password,
authenticate,
}
} }
const {
loading,
authenticateWithPassword,
errorMessage,
password,
authenticate,
} = useAuth()
</script> </script>

View File

@ -7,7 +7,7 @@
</div> </div>
</template> </template>
<script setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import ShowTasks from './ShowTasks' import ShowTasks from './ShowTasks'

View File

@ -238,7 +238,7 @@
</h3> </h3>
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<list-search @selected="changeList" ref="moveList"/> <list-search @update:modelValue="changeList" ref="moveList"/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,103 +1,97 @@
<template> <template>
<div> <div>
<h2 class="title has-text-centered">Login</h2> <message variant="success" class="has-text-centered" v-if="confirmedEmailSuccess">
<div class="box"> {{ $t('user.auth.confirmEmailSuccess') }}
<message variant="success" class="has-text-centered" v-if="confirmedEmailSuccess"> </message>
{{ $t('user.auth.confirmEmailSuccess') }} <message variant="danger" v-if="errorMessage">
</message> {{ errorMessage }}
<api-config @foundApi="hasApiUrl = true"/> </message>
<form @submit.prevent="submit" id="loginform" v-if="hasApiUrl && localAuthEnabled"> <form @submit.prevent="submit" id="loginform" v-if="localAuthEnabled">
<div class="field"> <div class="field">
<label class="label" for="username">{{ $t('user.auth.usernameEmail') }}</label> <label class="label" for="username">{{ $t('user.auth.usernameEmail') }}</label>
<div class="control"> <div class="control">
<input <input
class="input" id="username" class="input" id="username"
name="username" name="username"
:placeholder="$t('user.auth.usernamePlaceholder')" :placeholder="$t('user.auth.usernamePlaceholder')"
ref="username" ref="username"
required required
type="text" type="text"
autocomplete="username" autocomplete="username"
v-focus v-focus
@keyup.enter="submit" @keyup.enter="submit"
/> />
</div>
</div> </div>
<div class="field"> </div>
<label class="label" for="password">{{ $t('user.auth.password') }}</label> <div class="field">
<div class="control"> <label class="label" for="password">{{ $t('user.auth.password') }}</label>
<input <div class="control">
class="input" <input
id="password" class="input"
name="password" id="password"
:placeholder="$t('user.auth.passwordPlaceholder')" name="password"
ref="password" :placeholder="$t('user.auth.passwordPlaceholder')"
required ref="password"
type="password" required
autocomplete="current-password" type="password"
@keyup.enter="submit" autocomplete="current-password"
/> @keyup.enter="submit"
</div> />
</div> </div>
<div class="field" v-if="needsTotpPasscode"> </div>
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label> <div class="field" v-if="needsTotpPasscode">
<div class="control"> <label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
<input <div class="control">
autocomplete="one-time-code" <input
class="input" autocomplete="one-time-code"
id="totpPasscode" class="input"
:placeholder="$t('user.auth.totpPlaceholder')" id="totpPasscode"
ref="totpPasscode" :placeholder="$t('user.auth.totpPlaceholder')"
required ref="totpPasscode"
type="text" required
v-focus type="text"
@keyup.enter="submit" v-focus
/> @keyup.enter="submit"
</div> />
</div> </div>
<div class="field is-grouped login-buttons">
<div class="control is-expanded">
<x-button
@click="submit"
:loading="loading"
>
{{ $t('user.auth.login') }}
</x-button>
<x-button
:to="{ name: 'user.register' }"
v-if="registrationEnabled"
type="secondary"
>
{{ $t('user.auth.register') }}
</x-button>
</div>
<div class="control">
<router-link :to="{ name: 'user.password-reset.request' }" class="reset-password-link">
{{ $t('user.auth.resetPassword') }}
</router-link>
</div>
</div>
<message variant="danger" v-if="errorMessage">
{{ errorMessage }}
</message>
</form>
<div
v-if="hasOpenIdProviders"
class="mt-4">
<x-button
@click="redirectToProvider(p)"
v-for="(p, k) in openidConnect.providers"
:key="k"
type="secondary"
class="is-fullwidth mt-2"
>
{{ $t('user.auth.loginWith', {provider: p.name}) }}
</x-button>
</div> </div>
<legal/> <div class="field is-grouped login-buttons">
<div class="control is-expanded">
<x-button
@click="submit"
:loading="loading"
>
{{ $t('user.auth.login') }}
</x-button>
<x-button
:to="{ name: 'user.register' }"
v-if="registrationEnabled"
type="secondary"
>
{{ $t('user.auth.register') }}
</x-button>
</div>
<div class="control">
<router-link :to="{ name: 'user.password-reset.request' }" class="reset-password-link">
{{ $t('user.auth.forgotPassword') }}
</router-link>
</div>
</div>
</form>
<div
v-if="hasOpenIdProviders"
class="mt-4">
<x-button
@click="redirectToProvider(p)"
v-for="(p, k) in openidConnect.providers"
:key="k"
type="secondary"
class="is-fullwidth mt-2"
>
{{ $t('user.auth.loginWith', {provider: p.name}) }}
</x-button>
</div> </div>
</div> </div>
</template> </template>
@ -107,8 +101,6 @@ import {mapState} from 'vuex'
import {HTTPFactory} from '@/http-common' import {HTTPFactory} from '@/http-common'
import {LOADING} from '@/store/mutation-types' import {LOADING} from '@/store/mutation-types'
import legal from '../../components/misc/legal'
import ApiConfig from '@/components/misc/api-config.vue'
import {getErrorText} from '@/message' import {getErrorText} from '@/message'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
import {redirectToProvider} from '../../helpers/redirectToProvider' import {redirectToProvider} from '../../helpers/redirectToProvider'
@ -117,13 +109,10 @@ import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
export default { export default {
components: { components: {
Message, Message,
ApiConfig,
legal,
}, },
data() { data() {
return { return {
confirmedEmailSuccess: false, confirmedEmailSuccess: false,
hasApiUrl: false,
errorMessage: '', errorMessage: '',
} }
}, },
@ -161,13 +150,11 @@ export default {
} }
}, },
created() { created() {
this.hasApiUrl = window.API_URL !== ''
this.setTitle(this.$t('user.auth.login')) this.setTitle(this.$t('user.auth.login'))
}, },
computed: { computed: {
hasOpenIdProviders() { hasOpenIdProviders() {
return this.hasApiUrl && return this.openidConnect.enabled &&
this.openidConnect.enabled &&
this.openidConnect.providers && this.openidConnect.providers &&
this.openidConnect.providers.length > 0 this.openidConnect.providers.length > 0
}, },

View File

@ -1,67 +1,60 @@
<template> <template>
<div> <div>
<h2 class="title has-text-centered">{{ $t('user.auth.resetPassword') }}</h2> <message v-if="errorMsg">
<div class="box"> {{ errorMsg }}
<form @submit.prevent="submit" id="form" v-if="!successMessage"> </message>
<div class="field"> <div class="has-text-centered" v-if="successMessage">
<label class="label" for="password1">{{ $t('user.auth.password') }}</label> <message variant="success">
<div class="control"> {{ successMessage }}
<input </message>
class="input" <x-button :to="{ name: 'user.login' }">
id="password1" {{ $t('user.auth.login') }}
name="password1" </x-button>
:placeholder="$t('user.auth.passwordPlaceholder')"
required
type="password"
autocomplete="new-password"
v-focus
v-model="credentials.password"/>
</div>
</div>
<div class="field">
<label class="label" for="password2">{{ $t('user.auth.passwordRepeat') }}</label>
<div class="control">
<input
class="input"
id="password2"
name="password2"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
type="password"
autocomplete="new-password"
v-model="credentials.password2"
@keyup.enter="submit"
/>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<x-button
:loading="this.passwordResetService.loading"
@click="submit"
>
{{ $t('user.auth.resetPassword') }}
</x-button>
</div>
</div>
<message v-if="this.passwordResetService.loading">
{{ $t('misc.loading') }}
</message>
<message v-if="errorMsg">
{{ errorMsg }}
</message>
</form>
<div class="has-text-centered" v-if="successMessage">
<message variant="success">
{{ successMessage }}
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
</div>
<Legal/>
</div> </div>
<form @submit.prevent="submit" id="form" v-if="!successMessage">
<div class="field">
<label class="label" for="password1">{{ $t('user.auth.password') }}</label>
<div class="control">
<input
class="input"
id="password1"
name="password1"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
type="password"
autocomplete="new-password"
v-focus
v-model="credentials.password"/>
</div>
</div>
<div class="field">
<label class="label" for="password2">{{ $t('user.auth.passwordRepeat') }}</label>
<div class="control">
<input
class="input"
id="password2"
name="password2"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
type="password"
autocomplete="new-password"
v-model="credentials.password2"
@keyup.enter="submit"
/>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<x-button
:loading="this.passwordResetService.loading"
@click="submit"
>
{{ $t('user.auth.resetPassword') }}
</x-button>
</div>
</div>
</form>
</div> </div>
</template> </template>
@ -69,14 +62,11 @@
import {ref, reactive} from 'vue' import {ref, reactive} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import Legal from '@/components/misc/legal'
import PasswordResetModel from '@/models/passwordReset' import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset' import PasswordResetService from '@/services/passwordReset'
import {useTitle} from '@/composables/useTitle'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
const {t} = useI18n() const {t} = useI18n()
useTitle(() => t('user.auth.resetPassword'))
const credentials = reactive({ const credentials = reactive({
password: '', password: '',

View File

@ -1,97 +1,90 @@
<template> <template>
<div> <div>
<h2 class="title has-text-centered">{{ $t('user.auth.register') }}</h2> <message variant="danger" v-if="errorMessage !== ''">
<div class="box"> {{ errorMessage }}
<form @submit.prevent="submit" id="registerform"> </message>
<div class="field"> <form @submit.prevent="submit" id="registerform">
<label class="label" for="username">{{ $t('user.auth.username') }}</label> <div class="field">
<div class="control"> <label class="label" for="username">{{ $t('user.auth.username') }}</label>
<input <div class="control">
class="input" <input
id="username" class="input"
name="username" id="username"
:placeholder="$t('user.auth.usernamePlaceholder')" name="username"
required :placeholder="$t('user.auth.usernamePlaceholder')"
type="text" required
autocomplete="username" type="text"
v-focus autocomplete="username"
v-model="credentials.username" v-focus
@keyup.enter="submit" v-model="credentials.username"
/> @keyup.enter="submit"
</div> />
</div> </div>
<div class="field"> </div>
<label class="label" for="email">{{ $t('user.auth.email') }}</label> <div class="field">
<div class="control"> <label class="label" for="email">{{ $t('user.auth.email') }}</label>
<input <div class="control">
class="input" <input
id="email" class="input"
name="email" id="email"
:placeholder="$t('user.auth.emailPlaceholder')" name="email"
required :placeholder="$t('user.auth.emailPlaceholder')"
type="email" required
v-model="credentials.email" type="email"
@keyup.enter="submit" v-model="credentials.email"
/> @keyup.enter="submit"
</div> />
</div> </div>
<div class="field"> </div>
<label class="label" for="password">{{ $t('user.auth.password') }}</label> <div class="field">
<div class="control"> <label class="label" for="password">{{ $t('user.auth.password') }}</label>
<input <div class="control">
class="input" <input
id="password" class="input"
name="password" id="password"
:placeholder="$t('user.auth.passwordPlaceholder')" name="password"
required :placeholder="$t('user.auth.passwordPlaceholder')"
type="password" required
autocomplete="new-password" type="password"
v-model="credentials.password" autocomplete="new-password"
@keyup.enter="submit" v-model="credentials.password"
/> @keyup.enter="submit"
</div> />
</div> </div>
<div class="field"> </div>
<label class="label" for="passwordValidation">{{ $t('user.auth.passwordRepeat') }}</label> <div class="field">
<div class="control"> <label class="label" for="passwordValidation">{{ $t('user.auth.passwordRepeat') }}</label>
<input <div class="control">
class="input" <input
id="passwordValidation" class="input"
name="passwordValidation" id="passwordValidation"
:placeholder="$t('user.auth.passwordPlaceholder')" name="passwordValidation"
required :placeholder="$t('user.auth.passwordPlaceholder')"
type="password" required
autocomplete="new-password" type="password"
v-model="passwordValidation" autocomplete="new-password"
@keyup.enter="submit" v-model="passwordValidation"
/> @keyup.enter="submit"
</div> />
</div> </div>
</div>
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<x-button <x-button
:loading="loading" :loading="loading"
id="register-submit" id="register-submit"
@click="submit" @click="submit"
class="mr-2" class="mr-2"
> >
{{ $t('user.auth.register') }} {{ $t('user.auth.register') }}
</x-button> </x-button>
<x-button :to="{ name: 'user.login' }" type="secondary"> <x-button :to="{ name: 'user.login' }" type="secondary">
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
</x-button> </x-button>
</div>
</div> </div>
<message v-if="loading"> </div>
{{ $t('misc.loading') }} </form>
</message>
<message variant="danger" v-if="errorMessage !== ''">
{{ errorMessage }}
</message>
</form>
<legal/>
</div>
</div> </div>
</template> </template>
@ -101,8 +94,6 @@ import {useI18n} from 'vue-i18n'
import router from '@/router' import router from '@/router'
import {store} from '@/store' import {store} from '@/store'
import {useTitle} from '@/composables/useTitle'
import Legal from '@/components/misc/legal'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
// FIXME: use the `beforeEnter` hook of vue-router // FIXME: use the `beforeEnter` hook of vue-router
@ -114,7 +105,6 @@ onBeforeMount(() => {
}) })
const {t} = useI18n() const {t} = useI18n()
useTitle(() => t('user.auth.register'))
const credentials = reactive({ const credentials = reactive({
username: '', username: '',

View File

@ -1,67 +1,56 @@
<template> <template>
<div> <div>
<h2 class="title has-text-centered">{{ $t('user.auth.resetPassword') }}</h2> <message variant="danger" v-if="errorMsg">
<div class="box"> {{ errorMsg }}
<form @submit.prevent="submit" v-if="!isSuccess"> </message>
<div class="field"> <div class="has-text-centered" v-if="isSuccess">
<label class="label" for="email">{{ $t('user.auth.email') }}</label> <message variant="success">
<div class="control"> {{ $t('user.auth.resetPasswordSuccess') }}
<input </message>
class="input" <x-button :to="{ name: 'user.login' }">
id="email" {{ $t('user.auth.login') }}
name="email" </x-button>
:placeholder="$t('user.auth.emailPlaceholder')"
required
type="email"
v-focus
v-model="passwordReset.email"/>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<x-button
@click="submit"
:loading="passwordResetService.loading"
>
{{ $t('user.auth.resetPasswordAction') }}
</x-button>
<x-button :to="{ name: 'user.login' }" type="secondary">
{{ $t('user.auth.login') }}
</x-button>
</div>
</div>
<message variant="danger" v-if="errorMsg">
{{ errorMsg }}
</message>
</form>
<div class="has-text-centered" v-if="isSuccess">
<message variant="success">
{{ $t('user.auth.resetPasswordSuccess') }}
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
</div>
<Legal />
</div> </div>
<form @submit.prevent="submit" v-if="!isSuccess">
<div class="field">
<label class="label" for="email">{{ $t('user.auth.email') }}</label>
<div class="control">
<input
class="input"
id="email"
name="email"
:placeholder="$t('user.auth.emailPlaceholder')"
required
type="email"
v-focus
v-model="passwordReset.email"/>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<x-button
@click="submit"
:loading="passwordResetService.loading"
>
{{ $t('user.auth.resetPasswordAction') }}
</x-button>
<x-button :to="{ name: 'user.login' }" type="secondary">
{{ $t('user.auth.login') }}
</x-button>
</div>
</div>
</form>
</div> </div>
</template> </template>
<script setup> <script setup>
import {ref, reactive} from 'vue' import {ref, reactive} from 'vue'
import { useI18n } from 'vue-i18n'
import Legal from '@/components/misc/legal'
import PasswordResetModel from '@/models/passwordReset' import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset' import PasswordResetService from '@/services/passwordReset'
import { useTitle } from '@/composables/useTitle'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
const { t } = useI18n()
useTitle(() => t('user.auth.resetPassword'))
// Not sure if this instance needs a shalloRef at all // Not sure if this instance needs a shalloRef at all
const passwordResetService = reactive(new PasswordResetService()) const passwordResetService = reactive(new PasswordResetService())
const passwordReset = ref(new PasswordResetModel()) const passwordReset = ref(new PasswordResetModel())
@ -73,7 +62,7 @@ async function submit() {
try { try {
await passwordResetService.requestResetPassword(passwordReset.value) await passwordResetService.requestResetPassword(passwordReset.value)
isSuccess.value = true isSuccess.value = true
} catch(e) { } catch (e) {
errorMsg.value = e.response.data.message errorMsg.value = e.response.data.message
} }
} }

View File

@ -131,8 +131,10 @@ import {playPop} from '@/helpers/playPop'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {success} from '@/message' import {success} from '@/message'
const DEFAULT_LIST_ID = 0
function useColorSchemeSetting() { function useColorSchemeSetting() {
const { t } = useI18n() const {t} = useI18n()
const colorSchemeSettings = computed(() => ({ const colorSchemeSettings = computed(() => ({
light: t('user.settings.appearance.colorScheme.light'), light: t('user.settings.appearance.colorScheme.light'),
auto: t('user.settings.appearance.colorScheme.system'), auto: t('user.settings.appearance.colorScheme.system'),
@ -141,9 +143,11 @@ function useColorSchemeSetting() {
const {store} = useColorScheme() const {store} = useColorScheme()
watch(store, (schemeId) => { watch(store, (schemeId) => {
success({message: t('user.settings.appearance.setSuccess', { success({
colorScheme: colorSchemeSettings.value[schemeId], message: t('user.settings.appearance.setSuccess', {
})}) colorScheme: colorSchemeSettings.value[schemeId],
}),
})
}) })
return { return {
@ -178,8 +182,13 @@ export default {
.map(l => ({code: l[0], title: l[1]})) .map(l => ({code: l[0], title: l[1]}))
.sort((a, b) => a.title.localeCompare(b.title)) .sort((a, b) => a.title.localeCompare(b.title))
}, },
defaultList() { defaultList: {
return this.$store.getters['lists/getListById'](this.settings.defaultListId) get() {
return this.$store.getters['lists/getListById'](this.settings.defaultListId)
},
set(l) {
this.settings.defaultListId = l ? l.id : DEFAULT_LIST_ID
},
}, },
}, },
@ -204,12 +213,13 @@ export default {
localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone) localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone)
saveLanguage(this.language) saveLanguage(this.language)
setQuickAddMagicMode(this.quickAddMagicMode) setQuickAddMagicMode(this.quickAddMagicMode)
this.settings.defaultListId = this.defaultList ? this.defaultList.id : 0
await this.userSettingsService.update(this.settings) const settings = {
this.$store.commit('auth/setUserSettings', {
...this.settings, ...this.settings,
}) }
await this.userSettingsService.update(settings)
this.$store.commit('auth/setUserSettings', settings)
this.$message.success({message: this.$t('user.settings.general.savedSuccess')}) this.$message.success({message: this.$t('user.settings.general.savedSuccess')})
}, },
}, },

1294
yarn.lock

File diff suppressed because it is too large Load Diff