Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,92 +1,136 @@
<template>

<MessageLayout
<StudioMessageLayout
:header="$tr('resetPasswordTitle')"
:text="$tr('resetPasswordPrompt')"
>
<VForm
ref="form"
lazy-validation
@submit.prevent="submit"
<form
class="reset-password-form"
novalidate
@submit.prevent="resetPassword"
>
<Banner
:text="$tr('resetPasswordFailed')"
:value="error"
<StudioBanner
v-if="error"
role="alert"
error
class="mb-4"
/>
<PasswordField
class="banner"
>
{{ $tr('resetPasswordFailed') }}
</StudioBanner>
<StudioPasswordField
v-model="new_password1"
:label="$tr('passwordLabel')"
:additionalRules="passwordValidationRules"
autofocus
:label="$tr('passwordLabel')"
:errorMessages="
touched.new_password1 && errors.new_password1 ? [new_password1ErrorText] : []
"
@blur="touched.new_password1 = true"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Blurring an empty field shows no "This field is required" error. Unstable's validate-on-blur showed it.

The blur handler only marks the field touched. Errors are only computed on input or on submit.

Run the field's validator on blur, or compute the errors from form instead of storing them.

/>
<PasswordField
<StudioPasswordField
v-model="new_password2"
:label="$tr('passwordConfirmLabel')"
:additionalRules="passwordConfirmRules"
:errorMessages="
touched.new_password2 && errors.new_password2 ? [new_password2ErrorText] : []
"
@blur="touched.new_password2 = true"
/>
<KButton
primary
class="w-100"
:text="$tr('submitButton')"
type="submit"
/>
</VForm>
</MessageLayout>
</form>
</StudioMessageLayout>

</template>


<script>

import { mapActions } from 'vuex';
import MessageLayout from '../../components/MessageLayout';
import PasswordField from 'shared/views/form/PasswordField';
import Banner from 'shared/views/Banner';
import StudioMessageLayout from '../../components/StudioMessageLayout';
import StudioPasswordField from '../../components/form/StudioPasswordField';
import StudioBanner from 'shared/views/StudioBanner';
import commonStrings from 'shared/translator';
import { generateFormMixin } from 'shared/mixins';

const formMixin = generateFormMixin({
new_password1: {
required: true,
validator: v => Boolean(v) && v.length >= 8,
},
new_password2: {
required: true,
validator: (v, vm) => Boolean(v) && v === vm.form.new_password1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: Editing the first password doesn't re-check the confirmation.

  • Type abcdefgh in both, change field 1 to abcdefghZZ, Tab: no mismatch error until Submit.
  • The reverse also fails: after fixing a mismatch in field 1, the stale "Passwords don't match" stays.

Unstable re-ran the confirmation check whenever the first password changed. The mixin's setter only validates the field being set.

Add a watcher on form.new_password1 that re-runs the new_password2 check once that field is touched. Create.vue has the same gap.

},
});

export default {
name: 'ResetPassword',
components: {
MessageLayout,
PasswordField,
Banner,
StudioMessageLayout,
StudioPasswordField,
StudioBanner,
},
mixins: [formMixin],
data() {
return {
new_password1: '',
new_password2: '',
error: false,
// Gates error display until blur, since formMixin's setters otherwise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: This comment's Create.vue/#5060 sentences will go stale once #5060 is decided. Cut them and keep the first sentence.

// mark errors on every keystroke. Create.vue has no equivalent gate,
// so the two forms validate differently; epic-level decision tracked
// on #5060.
touched: {
new_password1: false,
new_password2: false,
},
};
},
computed: {
passwordConfirmRules() {
return [value => (this.new_password1 === value ? true : this.$tr('passwordMatchMessage'))];
new_password1ErrorText() {
if (!this.new_password1) {
/* eslint-disable-next-line kolibri/vue-no-undefined-string-uses */
return commonStrings.$tr('fieldRequired');
}
return this.$tr('passwordValidationMessage');
},
passwordValidationRules() {
return [value => (value.length >= 8 ? true : this.$tr('passwordValidationMessage'))];
new_password2ErrorText() {
if (!this.new_password2) {
/* eslint-disable-next-line kolibri/vue-no-undefined-string-uses */
return commonStrings.$tr('fieldRequired');
}
return this.$tr('passwordMatchMessage');
},
},
methods: {
...mapActions('account', ['setPassword']),
submit() {
resetPassword() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: accounts/pages/__tests__/resetPassword.spec.js is unchanged. The AC requires the existing unit suite to be meaningfully updated.

Add tests for the new behavior:

  • the role="alert" banner when setPassword rejects
  • the required error on empty submit, for both fields
  • errors hidden while typing and shown on blur
  • leading/trailing spaces reaching setPassword untrimmed. A later switch to clean() would break this most easily.

The AccountsMain VTL tests from #6056 show the banner and blur patterns.

this.error = false;
if (this.$refs.form.validate()) {
const payload = {
...this.$route.query,
new_password1: this.new_password1,
new_password2: this.new_password2,
};
this.setPassword(payload)
.then(() => {
this.$router.push({
name: 'ResetPasswordSuccess',
});
})
.catch(() => {
this.error = true;
});
this.touched.new_password1 = true;
this.touched.new_password2 = true;

// Validate against this.form rather than formMixin's clean(), which
// trims every field. Passwords must keep the leading/trailing spaces
// the user typed, both here and in the payload below.
if (!this.validate(this.form)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: A failed submit announces nothing to screen-reader users. Focus stays where it was. KTextbox doesn't wire invalidText to aria-describedby or aria-invalid, so the errors are never read.

Move focus to the first invalid field with a ref and KTextbox's focus(). This isn't a regression: the old Vuetify form and AccountsMain.vue have the same gap.

return;
}

const payload = {
...this.$route.query,
new_password1: this.form.new_password1,
new_password2: this.form.new_password2,
};
this.setPassword(payload)
.then(() => {
this.$router.push({
name: 'ResetPasswordSuccess',
});
})
.catch(() => {
this.error = true;
});
},
},
$trs: {
Expand All @@ -106,6 +150,17 @@

<style lang="scss" scoped>

.reset-password-form {
width: 400px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: The form overflows a 360px viewport. Fields, Submit and the banner are clipped at the end edge ("…Please try agai") in both LTR and RTL.

The cause is .message-slot-container in StudioMessageLayout. It has no width, so it shrink-wraps to this form's 400px. The form's max-width: 100% then resolves against 400px.

Add width: 100% to .message-slot-container. Verified: the form then renders at 328px. This also fixes RequestNewActivationLink, which overflows the same way.

max-width: 100%;
text-align: left;
}

.banner {
width: 100%;
margin-bottom: 16px;
}

.w-100 {
width: 100%;
}
Expand Down
Loading