Compare commits

..

22 Commits

Author SHA1 Message Date
cd92d224a2 Merge branch 'main' into feature/login-improvements 2022-01-23 17:06:49 +00:00
19a161ff78
fix: password validation field in test 2022-01-08 13:49:07 +01:00
310578d349
Merge branch 'main' into feature/login-improvements
# Conflicts:
#	src/components/misc/no-auth-wrapper.vue
#	src/styles/components/_index.scss
#	src/views/user/Login.vue
#	src/views/user/Register.vue
2022-01-08 13:44:35 +01:00
9c5613ad98
fix: lint 2021-12-26 13:42:21 +01:00
0322daf4d4
feat: move password to separate component 2021-12-26 13:37:33 +01:00
6041ad1482
Merge branch 'main' into feature/login-improvements 2021-12-26 12:19:44 +01:00
9a3069c20d
fix: propType validation in message.vue 2021-12-21 16:07:38 +01:00
27cd9535bf
fix: remove @ts-ignore 2021-12-21 15:47:57 +01:00
c46273ca34
fix: remove unused var 2021-12-21 15:47:40 +01:00
a4ec41e937
fix: motd on mobile 2021-12-21 15:21:23 +01:00
3eb0d58f79
fix: add .vue suffix to fix typescript warning 2021-12-21 15:21:23 +01:00
5558d91f44
feat: change links to login / register pages 2021-12-21 15:21:23 +01:00
9c04fb4e40
fix: disable login button 2021-12-21 15:21:23 +01:00
1fc1c20c87
feat: add extra prop for message center text 2021-12-21 15:21:23 +01:00
a1814ea29d
fix: message spacing 2021-12-21 15:21:23 +01:00
fda0b81d9c
feat: add tooltip and aria-label 2021-12-21 15:21:22 +01:00
8397608fef
chore: move password field toggle to scss file 2021-12-21 15:21:22 +01:00
66d5e851e8
feat: improve error handling of login fields 2021-12-21 15:21:22 +01:00
1d916e7e03
feat: change wording 2021-12-21 15:21:22 +01:00
aa12bffcbc
feat: replace password comparison with password toggle 2021-12-21 15:21:22 +01:00
05e054f501
feat: improve input validation for register form 2021-12-21 15:21:20 +01:00
f7eb160509
fix: move forgot password link next to password label
In some languages, the texts on the "log in" and "register" buttons were so long they wrapped underneath each other. Moving the "forgot password" link next to the password label leaves these two buttons enough space to always stay next to each other.
2021-12-21 14:34:16 +01:00
26 changed files with 785 additions and 789 deletions

View File

@ -116,16 +116,36 @@ steps:
YARN_CACHE_FOLDER: .cache/yarn/
CYPRESS_CACHE_FOLDER: .cache/cypress/
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
CYPRESS_RECORD_KEY:
from_secret: cypress_project_key
commands:
- sed -i 's/localhost/api/g' dist/index.html
- yarn serve:dist & npx wait-on http://localhost:5000
- yarn test:frontend --browser chrome --record
- yarn test:frontend --browser chrome
depends_on:
- dependencies
- build-prod
- name: upload-test-results
image: plugins/s3
pull: true
settings:
bucket: drone-test-results
access_key:
from_secret: test_results_aws_access_key_id
secret_key:
from_secret: test_results_aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
path_style: true
source: cypress/screenshots/**/**/*
strip_prefix: cypress/screenshots/
target: /${DRONE_REPO}/${DRONE_PULL_REQUEST}_${DRONE_BRANCH}/${DRONE_BUILD_NUMBER}/
depends_on:
- test-frontend
when:
status:
- failure
- success
- name: deploy-preview
image: node:16
pull: true
@ -645,6 +665,6 @@ steps:
from_secret: crowdin_key
---
kind: signature
hmac: 997e1badebe484ac29557c4af356e63db4d3d57f3d32e92d482f117f8cec64da
hmac: 188ee90100c5fc5922a445e531e7a47453121edddb2a64a182eb23ed2bf602de
...

View File

@ -7,6 +7,5 @@
"video": false,
"retries": {
"runMode": 2
},
"projectId": "181c7x"
}
}

View File

@ -25,7 +25,6 @@ context('Registration', () => {
cy.get('#username').type(fixture.username)
cy.get('#email').type(fixture.email)
cy.get('#password').type(fixture.password)
cy.get('#passwordValidation').type(fixture.password)
cy.get('#register-submit').click()
cy.url().should('include', '/')
cy.clock(1625656161057) // 13:00
@ -43,7 +42,6 @@ context('Registration', () => {
cy.get('#username').type(fixture.username)
cy.get('#email').type(fixture.email)
cy.get('#password').type(fixture.password)
cy.get('#passwordValidation').type(fixture.password)
cy.get('#register-submit').click()
cy.get('div.message.danger').contains('A user with this username already exists.')
})

View File

@ -8,14 +8,12 @@ describe('User Settings', () => {
})
it('Changes the user avatar', () => {
cy.intercept(`${Cypress.env('API_URL')}/user/settings/avatar/upload`).as('uploadAvatar')
cy.visit('/user/settings/avatar')
cy.get('input[name=avatarProvider][value=upload]')
.click()
cy.get('input[type=file]', {timeout: 1000})
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
cy.get('input[type=file]', { timeout: 1000 })
.attachFile('image.jpg')
cy.get('.vue-handler-wrapper.vue-handler-wrapper--south .vue-simple-handler.vue-simple-handler--south')
.trigger('mousedown', {which: 1})
.trigger('mousemove', {clientY: 100})
@ -24,7 +22,7 @@ describe('User Settings', () => {
.contains('Upload Avatar')
.click()
cy.wait('@uploadAvatar')
cy.wait(3000) // Wait for the request to finish
cy.get('.global-notification')
.should('contain', 'Success')
})

View File

@ -1,5 +1,6 @@
import './commands'
import 'cypress-file-upload'
import '@4tw/cypress-drag-drop'
// see https://github.com/cypress-io/cypress/issues/702#issuecomment-587127275

View File

@ -18,20 +18,20 @@
"browserslist:update": "npx browserslist@latest --update-db"
},
"dependencies": {
"@github/hotkey": "2.0.0",
"@github/hotkey": "1.6.1",
"@kyvg/vue3-notification": "2.3.4",
"@sentry/tracing": "6.17.4",
"@sentry/vue": "6.17.4",
"@sentry/tracing": "6.16.1",
"@sentry/vue": "6.16.1",
"@types/is-touch-device": "1.0.0",
"@vue/compat": "3.2.29",
"@vueuse/core": "7.5.5",
"@vueuse/router": "7.5.5",
"@vueuse/core": "7.5.4",
"@vueuse/router": "7.5.4",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.1",
"copy-to-clipboard": "3.3.1",
"date-fns": "2.28.0",
"dompurify": "2.3.5",
"dompurify": "2.3.4",
"easymde": "2.16.1",
"flatpickr": "4.6.9",
"flexsearch": "0.7.21",
@ -39,16 +39,16 @@
"is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.0.12",
"marked": "4.0.10",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"ufo": "0.7.10",
"ufo": "0.7.9",
"v-tooltip": "4.0.0-beta.17",
"vue": "3.2.29",
"vue-advanced-cropper": "2.8.0",
"vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.5",
"vue-i18n": "9.2.0-beta.30",
"vue-i18n": "9.2.0-beta.28",
"vue-router": "4.0.12",
"vuedraggable": "4.1.0",
"vuex": "4.0.2",
@ -56,39 +56,40 @@
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.1.0",
"@faker-js/faker": "6.0.0-alpha.5",
"@faker-js/faker": "6.0.0-alpha.3",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/vue-fontawesome": "3.0.0-5",
"@types/flexsearch": "0.7.2",
"@typescript-eslint/eslint-plugin": "5.10.2",
"@typescript-eslint/parser": "5.10.2",
"@typescript-eslint/eslint-plugin": "5.10.0",
"@typescript-eslint/parser": "5.10.0",
"@vitejs/plugin-legacy": "1.6.4",
"@vitejs/plugin-vue": "2.1.0",
"@vue/eslint-config-typescript": "10.0.0",
"autoprefixer": "10.4.2",
"axios": "0.25.0",
"browserslist": "4.19.1",
"caniuse-lite": "1.0.30001307",
"cypress": "9.4.1",
"esbuild": "0.14.18",
"eslint": "8.8.0",
"eslint-plugin-vue": "8.4.1",
"caniuse-lite": "1.0.30001301",
"cypress": "9.3.1",
"cypress-file-upload": "5.0.8",
"esbuild": "0.14.13",
"eslint": "8.7.0",
"eslint-plugin-vue": "8.3.0",
"express": "4.17.2",
"happy-dom": "2.31.1",
"netlify-cli": "8.15.0",
"postcss": "8.4.6",
"postcss-preset-env": "7.3.1",
"rollup": "2.67.0",
"netlify-cli": "8.8.2",
"happy-dom": "2.28.0",
"postcss": "8.4.5",
"postcss-preset-env": "7.2.3",
"rollup": "2.66.0",
"rollup-plugin-visualizer": "5.5.4",
"sass": "1.49.7",
"sass": "1.49.0",
"slugify": "1.6.5",
"typescript": "4.5.5",
"vite": "2.7.13",
"vite-plugin-pwa": "0.11.13",
"vite-svg-loader": "3.1.2",
"vitest": "0.2.7",
"vitest": "0.2.0",
"vue-tsc": "0.31.1",
"wait-on": "6.0.0",
"workbox-cli": "6.4.2"

View File

@ -90,15 +90,13 @@
v-bind="dragOptions"
:modelValue="activeLists[nk]"
@update:modelValue="(lists) => updateActiveLists(n, lists)"
group="namespace-lists"
:group="`namespace-${n.id}-lists`"
@start="() => drag = true"
@end="saveListPosition"
@end="e => saveListPosition(e, nk)"
handle=".handle"
:disabled="n.id < 0 || null"
tag="transition-group"
item-key="id"
:data-namespace-id="n.id"
:data-namespace-index="nk"
:component-data="{
type: 'transition',
tag: 'ul',
@ -200,7 +198,7 @@ export default {
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
}),
activeLists() {
return this.namespaces.map(({lists}) => lists?.filter(item => typeof item !== 'undefined' && !item.isArchived))
return this.namespaces.map(({lists}) => lists?.filter(item => !item.isArchived))
},
namespaceTitles() {
return this.namespaces.map((namespace) => this.getNamespaceTitle(namespace))
@ -243,15 +241,15 @@ export default {
this.listsVisible[namespaceId] = !this.listsVisible[namespaceId]
},
updateActiveLists(namespace, activeLists) {
// This is a bit hacky: since we do have to filter out the archived items from the list
// this is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier.
const lists = [
...activeLists,
...namespace.lists.filter(l => l.isArchived),
]
// instead we iterate over the non archived items in the old list and replace them with the ones in their new order
const lists = namespace.lists.map((item) => {
if (item.isArchived) {
return item
}
return activeLists.shift()
})
const newNamespace = {
...namespace,
@ -261,11 +259,8 @@ export default {
this.$store.commit('namespaces/setNamespaceById', newNamespace)
},
async saveListPosition(e) {
const namespaceId = parseInt(e.to.dataset.namespaceId)
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex)
const listsActive = this.activeLists[newNamespaceIndex]
async saveListPosition(e, namespaceIndex) {
const listsActive = this.activeLists[namespaceIndex]
const list = listsActive[e.newIndex]
const listBefore = listsActive[e.newIndex - 1] ?? null
const listAfter = listsActive[e.newIndex + 1] ?? null
@ -278,7 +273,6 @@ export default {
await this.$store.dispatch('lists/updateList', {
...list,
position,
namespaceId,
})
} finally {
this.listUpdating[list.id] = false

View File

@ -0,0 +1,85 @@
<template>
<div class="password-field">
<input
class="input"
id="password"
name="password"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
:type="passwordFieldType"
autocomplete="current-password"
@keyup.enter="e => $emit('submit', e)"
:tabindex="props.tabindex"
@focusout="validate"
@input="handleInput"
/>
<a
@click="togglePasswordFieldType"
class="password-field-type-toggle"
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
</a>
</div>
<p class="help is-danger" v-if="!isValid">
{{ $t('user.auth.passwordRequired') }}
</p>
</template>
<script lang="ts" setup>
import {ref, watch} from 'vue'
import {useDebounceFn} from '@vueuse/core'
const props = defineProps({
tabindex: String,
modelValue: String,
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
validateInitially: Boolean,
})
const emit = defineEmits(['submit', 'update:modelValue'])
const passwordFieldType = ref<String>('password')
const password = ref<String>('')
const isValid = ref<Boolean>(!props.validateInitially)
watch(
() => props.validateInitially,
(doValidate: Boolean) => {
if (doValidate) {
validate()
}
},
)
function validate() {
useDebounceFn(() => {
isValid.value = password.value !== ''
}, 100)()
}
function togglePasswordFieldType() {
passwordFieldType.value = passwordFieldType.value === 'password'
? 'text'
: 'password'
}
function handleInput(e) {
password.value = e.target.value
emit('update:modelValue', e.target.value)
}
</script>
<style scoped>
.password-field {
position: relative;
}
.password-field-type-toggle {
position: absolute;
color: var(--grey-400);
top: 50%;
right: 1rem;
transform: translateY(-50%);
}
</style>

View File

@ -1,18 +1,35 @@
<template>
<div class="message-wrapper">
<div class="message" :class="variant">
<div class="message" :class="[variant, textAlignClass]">
<slot/>
</div>
</div>
</template>
<script lang="ts" setup>
defineProps({
import {computed, PropType} from 'vue'
const TEXT_ALIGN_MAP = Object.freeze({
left: '',
center: 'has-text-centered',
right: 'has-text-right',
})
type textAlignVariants = keyof typeof TEXT_ALIGN_MAP
const props = defineProps({
variant: {
type: String,
default: 'info',
},
textAlign: {
type: String as PropType<textAlignVariants>,
default: 'left',
},
})
const textAlignClass = computed(() => TEXT_ALIGN_MAP[props.textAlign])
</script>
<style lang="scss" scoped>

View File

@ -14,6 +14,9 @@
<div>
<h2 class="title" v-if="title">{{ title }}</h2>
<api-config/>
<Message v-if="motd !== ''" class="is-hidden-tablet mb-4">
{{ motd }}
</Message>
<slot/>
</div>
<legal/>
@ -38,8 +41,8 @@ const store = useStore()
const {t} = useI18n()
const motd = computed(() => store.state.config.motd)
// @ts-ignore
const title = computed(() => t(route.meta.title ?? ''))
const title = computed(() => t(route.meta?.title as string || ''))
useTitle(() => title.value)
</script>

View File

@ -1,7 +1,7 @@
<template>
<x-button
variant="secondary"
:icon="iconName"
:icon="icon"
v-tooltip="tooltipText"
@click="changeSubscription"
:disabled="disabled || null"
@ -16,7 +16,7 @@
v-else
>
<span class="icon">
<icon :icon="iconName"/>
<icon :icon="icon"/>
</span>
{{ buttonText }}
</a>
@ -73,7 +73,7 @@ const tooltipText = computed(() => {
})
const buttonText = computed(() => props.subscription !== null ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const iconName = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const icon = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => {
if (props.subscription === null) {
return false

View File

@ -274,11 +274,10 @@ export default {
return tasks
.map(task => {
// by doing this here once we can save a lot of duplicate calls in the template
const listAndNamespace = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
const {
list,
namespace,
} = listAndNamespace === null ? {list: null, namespace: null} : listAndNamespace
} = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
return {
...task,

6
src/helpers/isEmail.ts Normal file
View File

@ -0,0 +1,6 @@
export function isEmail(email: string): Boolean {
const format = /^.+@.+$/
const match = email.match(format)
return match === null ? false : match.length > 0
}

View File

@ -288,7 +288,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
}
const getDayFromText = (text: string) => {
const matcher = /($| )(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)($| )/ig
const matcher = /(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)/ig
const results = matcher.exec(text)
if (results === null) {
return {
@ -302,18 +302,18 @@ const getDayFromText = (text: string) => {
const day = parseInt(results[0])
date.setDate(day)
// If the parsed day is the 31st (or 29+ and the next month is february) but the next month only has 30 days,
// setting the day to 31 will "overflow" the date to the next month, but the first.
// If the parsed day is the 31st but the next month only has 30 days, setting the day to 31 will "overflow" the
// date to the next month, but the first.
// This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after
// setting it for the first time and set it again if it isn't - that would mean the month overflowed.
while (date < now) {
date.setMonth(date.getMonth() + 1)
}
if (date.getDate() !== day) {
if (day === 31 && date.getDate() !== day) {
date.setDate(day)
}
if (date < now) {
date.setMonth(date.getMonth() + 1)
}
return {
foundText: results[0],
date: date,

View File

@ -31,10 +31,9 @@
"username": "Username",
"usernameEmail": "Username Or Email Address",
"usernamePlaceholder": "e.g. frederick",
"email": "E-mail address",
"email": "Email address",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password",
@ -45,12 +44,19 @@
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"register": "Register",
"createAccount": "Create account",
"loginWith": "Log in with {provider}",
"authenticating": "Authenticating…",
"openIdStateError": "State does not match, refusing to continue!",
"openIdGeneralError": "An error occured while authenticating against the third party.",
"logout": "Logout"
"logout": "Logout",
"emailInvalid": "Please enter a valid email address.",
"usernameRequired": "Please provide a username.",
"passwordRequired": "Please provide a password.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
"alreadyHaveAnAccount": "Already have an account?"
},
"settings": {
"title": "Settings",
@ -61,7 +67,7 @@
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your E-Mail Address",
"updateEmailTitle": "Update Your Email Address",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {

View File

@ -16,6 +16,8 @@ import {
faCocktail,
faCoffee,
faCog,
faEye,
faEyeSlash,
faEllipsisH,
faEllipsisV,
faExclamation,
@ -87,6 +89,8 @@ library.add(faCocktail)
library.add(faCoffee)
library.add(faCog)
library.add(faComments)
library.add(faEye)
library.add(faEyeSlash)
library.add(faEllipsisH)
library.add(faEllipsisV)
library.add(faExclamation)

View File

@ -1,4 +1,4 @@
import {beforeEach, afterEach, describe, it, expect, vi} from 'vitest'
import {describe, it, expect} from 'vitest'
import {parseTaskText} from './parseTaskText'
import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate'
@ -6,14 +6,6 @@ import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
import priorities from '../models/constants/priorities.json'
describe('Parse Task Text', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('should return text with no intents as is', () => {
expect(parseTaskText('Lorem Ipsum').text).toBe('Lorem Ipsum')
})
@ -40,7 +32,7 @@ describe('Parse Task Text', () => {
expect(result.assignees).toHaveLength(1)
expect(result.assignees[0]).toBe('user')
})
it('should ignore email addresses', () => {
const text = 'Lorem Ipsum email@example.com'
const result = parseTaskText(text)
@ -219,36 +211,17 @@ describe('Parse Task Text', () => {
expect(`${result.date.getHours()}:${result.date.getMinutes()}`).toBe('14:0')
})
it('should recognize dates of the month in the past but next month', () => {
const time = new Date(2022, 0, 15)
vi.setSystemTime(time)
const result = parseTaskText(`Lorem Ipsum ${time.getDate() - 1}th`)
const date = new Date()
date.setDate(date.getDate() - 1)
const result = parseTaskText(`Lorem Ipsum ${date.getDate()}nd`)
expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(time.getDate() - 1)
expect(result.date.getMonth()).toBe(time.getMonth() + 1)
})
it('should recognize dates of the month in the past but next month when february is the next month', () => {
const jan = new Date(2022, 0, 30)
vi.setSystemTime(jan)
expect(result.date.getDate()).toBe(date.getDate())
const result = parseTaskText(`Lorem Ipsum ${jan.getDate() - 1}th`)
const expectedDate = new Date(2022, 2, jan.getDate() - 1)
expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(expectedDate.getDate())
expect(result.date.getMonth()).toBe(expectedDate.getMonth())
})
it('should recognize dates of the month in the past but next month when the next month has less days than this one', () => {
const mar = new Date(2022, 2, 32)
vi.setSystemTime(mar)
const result = parseTaskText(`Lorem Ipsum 31st`)
const expectedDate = new Date(2022, 4, 31)
expect(result.text).toBe('Lorem Ipsum')
expect(result.date.getDate()).toBe(expectedDate.getDate())
expect(result.date.getMonth()).toBe(expectedDate.getMonth())
const nextMonthWithDate = result.date.getDate() === 31
? (date.getMonth() + 2) % 12
: (date.getMonth() + 1) % 12
expect(result.date.getMonth()).toBe(nextMonthWithDate)
})
it('should recognize dates of the month in the future', () => {
const nextDay = new Date(+new Date() + 60 * 60 * 24 * 1000)
@ -269,12 +242,6 @@ describe('Parse Task Text', () => {
expect(result.text).toBe('Lorem Ipsum github')
expect(result.date).toBeNull()
})
it('should not recognize date number with no spacing around them', () => {
const result = parseTaskText('Lorem Ispum v1.1.1')
expect(result.text).toBe('Lorem Ispum v1.1.1')
expect(result.date).toBeNull()
})
describe('Parse weekdays', () => {

View File

@ -132,7 +132,7 @@ const router = createRouter({
name: 'user.register',
component: RegisterComponent,
meta: {
title: 'user.auth.register',
title: 'user.auth.createAccount',
},
},
{

View File

@ -23,6 +23,8 @@ export default {
return
}
// FIXME: direct manipulation of the prop
// might not be a problem since this is happening in the mutation
if (!namespace.lists || namespace.lists.length === 0) {
namespace.lists = state.namespaces[namespaceIndex].lists
}
@ -134,8 +136,8 @@ export default {
},
loadNamespacesIfFavoritesDontExist(ctx) {
// The first or second namespace should be the one holding all favorites
if (ctx.state.namespaces[0].id !== -2 && ctx.state.namespaces[1]?.id !== -2) {
// The first namespace should be the one holding all favorites
if (ctx.state.namespaces[0].id !== -2) {
return ctx.dispatch('loadNamespaces')
}
},

View File

@ -2,4 +2,4 @@
@import "labels";
@import "list";
@import "task";
@import "tasks";
@import "tasks";

View File

@ -4,11 +4,9 @@
@submit="archiveNamespace()"
>
<template #header><span>{{ title }}</span></template>
<template #text>
<p>
{{ namespace.isArchived ? $t('namespace.archive.unarchiveText') : $t('namespace.archive.archiveText')}}
</p>
<p>{{ list.isArchived ? $t('namespace.archive.unarchiveText') : $t('namespace.archive.archiveText') }}</p>
</template>
</modal>
</template>
@ -29,18 +27,17 @@ export default {
created() {
this.namespace = this.$store.getters['namespaces/getNamespaceById'](this.$route.params.id)
this.title = this.namespace.isArchived ?
this.$t('namespace.archive.titleUnarchive', {namespace: this.namespace.title}) :
this.$t('namespace.archive.titleArchive', {namespace: this.namespace.title})
this.$t('namespace.archive.titleUnarchive', { namespace: this.namespace.title }) :
this.$t('namespace.archive.titleArchive', { namespace: this.namespace.title })
this.setTitle(this.title)
},
methods: {
async archiveNamespace() {
this.namespace.isArchived = !this.namespace.isArchived
try {
const namespace = await this.namespaceService.update({
...this.namespace,
isArchived: !this.namespace.isArchived,
})
const namespace = await this.namespaceService.update(this.namespace)
this.$store.commit('namespaces/setNamespaceById', namespace)
this.$message.success({message: this.$t('namespace.archive.success')})
} finally {

View File

@ -1,9 +1,9 @@
<template>
<div>
<message variant="success" class="has-text-centered" v-if="confirmedEmailSuccess">
<message variant="success" text-align="center" class="mb-4" v-if="confirmedEmailSuccess">
{{ $t('user.auth.confirmEmailSuccess') }}
</message>
<message variant="danger" v-if="errorMessage">
<message variant="danger" v-if="errorMessage" class="mb-4">
{{ errorMessage }}
</message>
<form @submit.prevent="submit" id="loginform" v-if="localAuthEnabled">
@ -20,24 +20,26 @@
autocomplete="username"
v-focus
@keyup.enter="submit"
tabindex="1"
@focusout="validateField('username')"
/>
</div>
<p class="help is-danger" v-if="!usernameValid">
{{ $t('user.auth.usernameRequired') }}
</p>
</div>
<div class="field">
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
<div class="control">
<input
class="input"
id="password"
name="password"
:placeholder="$t('user.auth.passwordPlaceholder')"
ref="password"
required
type="password"
autocomplete="current-password"
@keyup.enter="submit"
/>
<div class="label-with-link">
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
<router-link
:to="{ name: 'user.password-reset.request' }"
class="reset-password-link"
tabindex="6"
>
{{ $t('user.auth.forgotPassword') }}
</router-link>
</div>
<password tabindex="2" @submit="submit" v-model="password" :validate-initially="validatePasswordInitially"/>
</div>
<div class="field" v-if="needsTotpPasscode">
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
@ -52,32 +54,28 @@
type="text"
v-focus
@keyup.enter="submit"
tabindex="3"
/>
</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"
variant="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>
<x-button
@click="submit"
:loading="loading"
tabindex="4"
>
{{ $t('user.auth.login') }}
</x-button>
<p class="mt-2" v-if="registrationEnabled">
{{ $t('user.auth.noAccountYet') }}
<router-link
:to="{ name: 'user.register' }"
type="secondary"
tabindex="5"
>
{{ $t('user.auth.createAccount') }}
</router-link>
</p>
</form>
<div
@ -97,6 +95,7 @@
</template>
<script>
import {useDebounceFn} from '@vueuse/core'
import {mapState} from 'vuex'
import {HTTPFactory} from '@/http-common'
@ -105,15 +104,20 @@ import {getErrorText} from '@/message'
import Message from '@/components/misc/message'
import {redirectToProvider} from '../../helpers/redirectToProvider'
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
import Password from '@/components/input/password'
export default {
components: {
Password,
Message,
},
data() {
return {
confirmedEmailSuccess: false,
errorMessage: '',
usernameValid: true,
password: '',
validatePasswordInitially: false,
}
},
beforeMount() {
@ -166,6 +170,13 @@ export default {
localAuthEnabled: state => state.config.auth.local.enabled,
openidConnect: state => state.config.auth.openidConnect,
}),
validateField() {
// using computed so that debounced function definition stays
return useDebounceFn((field) => {
this[`${field}Valid`] = this.$refs[field].value !== ''
}, 100)
},
},
methods: {
setLoading() {
@ -185,7 +196,14 @@ export default {
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
const credentials = {
username: this.$refs.username.value,
password: this.$refs.password.value,
password: this.password,
}
if (credentials.username === '' || credentials.password === '') {
// Trigger the validation error messages
this.validateField('username')
this.validatePasswordInitially = true
return
}
if (this.needsTotpPasscode) {
@ -196,7 +214,7 @@ export default {
await this.$store.dispatch('auth/login', credentials)
this.$store.commit('auth/needsTotpPasscode', false)
} catch (e) {
if (e.response && e.response.data.code === 1017 && !credentials.totpPasscode) {
if (e.response?.data.code === 1017 && !this.credentials.totpPasscode) {
return
}
@ -211,22 +229,21 @@ export default {
</script>
<style lang="scss" scoped>
.login-buttons {
@media screen and (max-width: 450px) {
flex-direction: column;
.control:first-child {
margin-bottom: 1rem;
}
}
}
.button {
margin: 0 0.4rem 0 0;
}
.reset-password-link {
display: inline-block;
padding-top: 5px;
}
.label-with-link {
display: flex;
justify-content: space-between;
margin-bottom: .5rem;
.label {
margin-bottom: 0;
}
}
</style>

View File

@ -1,9 +1,9 @@
<template>
<div>
<message v-if="errorMsg">
<message v-if="errorMsg" class="mb-4">
{{ errorMsg }}
</message>
<div class="has-text-centered" v-if="successMessage">
<div class="has-text-centered mb-4" v-if="successMessage">
<message variant="success">
{{ successMessage }}
</message>

View File

@ -1,6 +1,6 @@
<template>
<div>
<message variant="danger" v-if="errorMessage !== ''">
<message variant="danger" v-if="errorMessage !== ''" class="mb-4">
{{ errorMessage }}
</message>
<form @submit.prevent="submit" id="registerform">
@ -18,8 +18,12 @@
v-focus
v-model="credentials.username"
@keyup.enter="submit"
@focusout="validateUsername"
/>
</div>
<p class="help is-danger" v-if="!usernameValid">
{{ $t('user.auth.usernameRequired') }}
</p>
</div>
<div class="field">
<label class="label" for="email">{{ $t('user.auth.email') }}</label>
@ -33,68 +37,46 @@
type="email"
v-model="credentials.email"
@keyup.enter="submit"
@focusout="validateEmail"
/>
</div>
<p class="help is-danger" v-if="!emailValid">
{{ $t('user.auth.emailInvalid') }}
</p>
</div>
<div class="field">
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
<div class="control">
<input
class="input"
id="password"
name="password"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
type="password"
autocomplete="new-password"
v-model="credentials.password"
@keyup.enter="submit"
/>
</div>
</div>
<div class="field">
<label class="label" for="passwordValidation">{{ $t('user.auth.passwordRepeat') }}</label>
<div class="control">
<input
class="input"
id="passwordValidation"
name="passwordValidation"
:placeholder="$t('user.auth.passwordPlaceholder')"
required
type="password"
autocomplete="new-password"
v-model="passwordValidation"
@keyup.enter="submit"
/>
</div>
<password @submit="submit" @update:modelValue="v => credentials.password = v" :validate-initially="validatePasswordInitially"/>
</div>
<div class="field is-grouped">
<div class="control">
<x-button
:loading="loading"
id="register-submit"
@click="submit"
class="mr-2"
>
{{ $t('user.auth.register') }}
</x-button>
<x-button :to="{ name: 'user.login' }" variant="secondary">
{{ $t('user.auth.login') }}
</x-button>
</div>
</div>
<x-button
:loading="loading"
id="register-submit"
@click="submit"
class="mr-2"
:disabled="!everythingValid"
>
{{ $t('user.auth.createAccount') }}
</x-button>
<p class="mt-2">
{{ $t('user.auth.alreadyHaveAnAccount') }}
<router-link :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</router-link>
</p>
</form>
</div>
</template>
<script setup>
import {useDebounceFn} from '@vueuse/core'
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
import {useI18n} from 'vue-i18n'
import router from '@/router'
import {store} from '@/store'
import Message from '@/components/misc/message'
import {isEmail} from '@/helpers/isEmail'
import Password from '@/components/input/password'
// FIXME: use the `beforeEnter` hook of vue-router
// Check if the user is already logged in, if so, redirect them to the homepage
@ -104,27 +86,45 @@ onBeforeMount(() => {
}
})
const {t} = useI18n()
const credentials = reactive({
username: '',
email: '',
password: '',
})
const passwordValidation = ref('')
const loading = computed(() => store.state.loading)
const errorMessage = ref('')
const validatePasswordInitially = ref(false)
const DEBOUNCE_TIME = 100
// debouncing to prevent error messages when clicking on the log in button
const emailValid = ref(true)
const validateEmail = useDebounceFn(() => {
emailValid.value = isEmail(credentials.email)
}, DEBOUNCE_TIME)
const usernameValid = ref(true)
const validateUsername = useDebounceFn(() => {
usernameValid.value = credentials.username !== ''
}, DEBOUNCE_TIME)
const everythingValid = computed(() => {
return credentials.username !== '' &&
credentials.email !== '' &&
credentials.password !== '' &&
emailValid.value &&
usernameValid.value
})
async function submit() {
errorMessage.value = ''
validatePasswordInitially.value = true
if (credentials.password !== passwordValidation.value) {
errorMessage.value = t('user.auth.passwordsDontMatch')
if (!everythingValid.value) {
return
}
try {
await store.dispatch('auth/register', toRaw(credentials))
} catch (e) {

View File

@ -1,9 +1,9 @@
<template>
<div>
<message variant="danger" v-if="errorMsg">
<message variant="danger" v-if="errorMsg" class="mb-4">
{{ errorMsg }}
</message>
<div class="has-text-centered" v-if="isSuccess">
<div class="has-text-centered mb-4" v-if="isSuccess">
<message variant="success">
{{ $t('user.auth.resetPasswordSuccess') }}
</message>

978
yarn.lock

File diff suppressed because it is too large Load Diff