UX: don't disable "create account" button & display error message for required fields. (#9643)

This commit is contained in:
Vinoth Kannan 2020-05-14 12:15:33 +05:30 committed by GitHub
parent 3d050bdaa3
commit c014b93854
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 162 additions and 112 deletions

View File

@ -6,6 +6,16 @@ export default Component.extend({
classNameBindings: [":user-field", "field.field_type", "customFieldClass"], classNameBindings: [":user-field", "field.field_type", "customFieldClass"],
layoutName: fmt("field.field_type", "components/user-fields/%@"), layoutName: fmt("field.field_type", "components/user-fields/%@"),
didInsertElement() {
this._super(...arguments);
let element = this.element.querySelector(
".user-field.dropdown .select-kit-header"
);
element = element || this.element.querySelector("input");
this.field.element = element;
},
@discourseComputed @discourseComputed
noneLabel() { noneLabel() {
return "user_fields.none"; return "user_fields.none";

View File

@ -61,27 +61,9 @@ export default Controller.extend(
this._createUserFields(); this._createUserFields();
}, },
@discourseComputed( @discourseComputed("formSubmitted")
"passwordRequired",
"nameValidation.failed",
"emailValidation.failed",
"usernameValidation.failed",
"passwordValidation.failed",
"userFieldsValidation.failed",
"formSubmitted",
"inviteCode"
)
submitDisabled() { submitDisabled() {
if (this.formSubmitted) return true; if (this.formSubmitted) return true;
if (this.get("nameValidation.failed")) return true;
if (this.get("emailValidation.failed")) return true;
if (this.get("usernameValidation.failed") && this.usernameRequired)
return true;
if (this.get("passwordValidation.failed") && this.passwordRequired)
return true;
if (this.get("userFieldsValidation.failed")) return true;
if (this.requireInviteCode && !this.inviteCode) return true;
return false; return false;
}, },
@ -114,18 +96,26 @@ export default Controller.extend(
// Check the email address // Check the email address
@discourseComputed("accountEmail", "rejectedEmails.[]") @discourseComputed("accountEmail", "rejectedEmails.[]")
emailValidation(email, rejectedEmails) { emailValidation(email, rejectedEmails) {
const failedAttrs = {
failed: true,
element: document.querySelector("#new-account-email")
};
// If blank, fail without a reason // If blank, fail without a reason
if (isEmpty(email)) { if (isEmpty(email)) {
return EmberObject.create({ return EmberObject.create(
failed: true Object.assign(failedAttrs, {
}); message: I18n.t("user.email.required")
})
);
} }
if (rejectedEmails.includes(email)) { if (rejectedEmails.includes(email)) {
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.email.invalid") reason: I18n.t("user.email.invalid")
}); })
);
} }
if ( if (
@ -149,10 +139,11 @@ export default Controller.extend(
}); });
} }
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.email.invalid") reason: I18n.t("user.email.invalid")
}); })
);
}, },
@discourseComputed( @discourseComputed(
@ -312,6 +303,34 @@ export default Controller.extend(
}, },
createAccount() { createAccount() {
this.clearFlash();
const validation = [
this.emailValidation,
this.usernameValidation,
this.nameValidation,
this.passwordValidation,
this.userFieldsValidation
].find(v => v.failed);
if (validation) {
if (validation.message) {
this.flash(validation.message, "error");
}
const element = validation.element;
if (element.tagName === "DIV") {
if (element.scrollIntoView) {
element.scrollIntoView();
}
element.click();
} else {
element.focus();
}
return;
}
if (new Date() - this._challengeDate > 1000 * this._challengeExpiry) { if (new Date() - this._challengeDate > 1000 * this._challengeExpiry) {
this.fetchConfirmationValue().then(() => this.fetchConfirmationValue().then(() =>
this.performAccountCreation() this.performAccountCreation()

View File

@ -18,7 +18,11 @@ export default Mixin.create({
@discourseComputed("accountName") @discourseComputed("accountName")
nameValidation() { nameValidation() {
if (this.siteSettings.full_name_required && isEmpty(this.accountName)) { if (this.siteSettings.full_name_required && isEmpty(this.accountName)) {
return EmberObject.create({ failed: true }); return EmberObject.create({
failed: true,
message: I18n.t("user.name.required"),
element: document.querySelector("#new-account-name")
});
} }
return EmberObject.create({ ok: true }); return EmberObject.create({ ok: true });

View File

@ -43,44 +43,57 @@ export default Mixin.create({
accountEmail, accountEmail,
passwordMinLength passwordMinLength
) { ) {
const failedAttrs = {
failed: true,
element: document.querySelector("#new-account-password")
};
if (!passwordRequired) { if (!passwordRequired) {
return EmberObject.create({ ok: true }); return EmberObject.create({ ok: true });
} }
if (rejectedPasswords.includes(password)) { if (rejectedPasswords.includes(password)) {
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: reason:
this.rejectedPasswordsMessages.get(password) || this.rejectedPasswordsMessages.get(password) ||
I18n.t("user.password.common") I18n.t("user.password.common")
}); })
);
} }
// If blank, fail without a reason // If blank, fail without a reason
if (isEmpty(password)) { if (isEmpty(password)) {
return EmberObject.create({ failed: true }); return EmberObject.create(
Object.assign(failedAttrs, {
message: I18n.t("user.password.required")
})
);
} }
// If too short // If too short
if (password.length < passwordMinLength) { if (password.length < passwordMinLength) {
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.password.too_short") reason: I18n.t("user.password.too_short")
}); })
);
} }
if (!isEmpty(accountUsername) && password === accountUsername) { if (!isEmpty(accountUsername) && password === accountUsername) {
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.password.same_as_username") reason: I18n.t("user.password.same_as_username")
}); })
);
} }
if (!isEmpty(accountEmail) && password === accountEmail) { if (!isEmpty(accountEmail) && password === accountEmail) {
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.password.same_as_email") reason: I18n.t("user.password.same_as_email")
}); })
);
} }
// Looks good! // Looks good!

View File

@ -27,12 +27,17 @@ export default Mixin.create({
userFields = userFields.filterBy("field.required"); userFields = userFields.filterBy("field.required");
} }
if (!isEmpty(userFields)) { if (!isEmpty(userFields)) {
const anyEmpty = userFields.any(uf => { const emptyUserField = userFields.find(uf => {
const val = uf.get("value"); const val = uf.get("value");
return !val || isEmpty(val); return !val || isEmpty(val);
}); });
if (anyEmpty) { if (emptyUserField) {
return EmberObject.create({ failed: true }); const userField = emptyUserField.field;
return EmberObject.create({
failed: true,
message: I18n.t("user_fields.required", { name: userField.name }),
element: userField.element
});
} }
} }
return EmberObject.create({ ok: true }); return EmberObject.create({ ok: true });

View File

@ -31,6 +31,10 @@ export default Mixin.create({
@discourseComputed("accountUsername") @discourseComputed("accountUsername")
basicUsernameValidation(accountUsername) { basicUsernameValidation(accountUsername) {
const failedAttrs = {
failed: true,
element: document.querySelector("#new-account-username")
};
this.set("uniqueUsernameValidation", null); this.set("uniqueUsernameValidation", null);
if (accountUsername && accountUsername === this.prefilledUsername) { if (accountUsername && accountUsername === this.prefilledUsername) {
@ -42,31 +46,38 @@ export default Mixin.create({
// If blank, fail without a reason // If blank, fail without a reason
if (isEmpty(accountUsername)) { if (isEmpty(accountUsername)) {
return EmberObject.create({ failed: true }); return EmberObject.create(
Object.assign(failedAttrs, {
message: I18n.t("user.username.required")
})
);
} }
// If too short // If too short
if (accountUsername.length < this.siteSettings.min_username_length) { if (accountUsername.length < this.siteSettings.min_username_length) {
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.username.too_short") reason: I18n.t("user.username.too_short")
}); })
);
} }
// If too long // If too long
if (accountUsername.length > this.maxUsernameLength) { if (accountUsername.length > this.maxUsernameLength) {
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.username.too_long") reason: I18n.t("user.username.too_long")
}); })
);
} }
this.checkUsernameAvailability(); this.checkUsernameAvailability();
// Let's check it out asynchronously // Let's check it out asynchronously
return EmberObject.create({ return EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.username.checking") reason: I18n.t("user.username.checking")
}); })
);
}, },
shouldCheckUsernameAvailability() { shouldCheckUsernameAvailability() {
@ -93,23 +104,30 @@ export default Mixin.create({
}) })
); );
} else { } else {
const failedAttrs = {
failed: true,
element: document.querySelector("#new-account-username")
};
if (result.suggestion) { if (result.suggestion) {
return this.set( return this.set(
"uniqueUsernameValidation", "uniqueUsernameValidation",
EmberObject.create({ EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: I18n.t("user.username.not_available", result) reason: I18n.t("user.username.not_available", result)
}) })
)
); );
} else { } else {
return this.set( return this.set(
"uniqueUsernameValidation", "uniqueUsernameValidation",
EmberObject.create({ EmberObject.create(
failed: true, Object.assign(failedAttrs, {
reason: result.errors reason: result.errors
? result.errors.join(" ") ? result.errors.join(" ")
: I18n.t("user.username.not_available_no_suggestion") : I18n.t("user.username.not_available_no_suggestion")
}) })
)
); );
} }
} }

View File

@ -39,6 +39,7 @@
#modal-alert { #modal-alert {
max-width: 100%; max-width: 100%;
margin-bottom: 0;
padding: 8px 16px; padding: 8px 16px;
} }

View File

@ -821,6 +821,7 @@ en:
user_fields: user_fields:
none: "(select an option)" none: "(select an option)"
required: 'Please enter a value for "%{name}"'
user: user:
said: "{{username}}:" said: "{{username}}:"
@ -1115,6 +1116,7 @@ en:
sso_override_instructions: "Email can be updated from SSO provider." sso_override_instructions: "Email can be updated from SSO provider."
instructions: "Never shown to the public." instructions: "Never shown to the public."
ok: "We will email you to confirm" ok: "We will email you to confirm"
required: "Please enter an email address"
invalid: "Please enter a valid email address" invalid: "Please enter a valid email address"
authenticated: "Your email has been authenticated by {{provider}}" authenticated: "Your email has been authenticated by {{provider}}"
frequency_immediately: "We'll email you immediately if you haven't read the thing we're emailing you about." frequency_immediately: "We'll email you immediately if you haven't read the thing we're emailing you about."
@ -1137,6 +1139,7 @@ en:
title: "Name" title: "Name"
instructions: "your full name (optional)" instructions: "your full name (optional)"
instructions_required: "Your full name" instructions_required: "Your full name"
required: "Please enter a name"
too_short: "Your name is too short" too_short: "Your name is too short"
ok: "Your name looks good" ok: "Your name looks good"
username: username:
@ -1150,6 +1153,7 @@ en:
too_long: "Your username is too long" too_long: "Your username is too long"
checking: "Checking username availability..." checking: "Checking username availability..."
prefilled: "Email matches this registered username" prefilled: "Email matches this registered username"
required: "Please enter a username"
locale: locale:
title: "Interface language" title: "Interface language"
@ -1308,6 +1312,7 @@ en:
same_as_email: "Your password is the same as your email." same_as_email: "Your password is the same as your email."
ok: "Your password looks good." ok: "Your password looks good."
instructions: "at least %{count} characters" instructions: "at least %{count} characters"
required: "Please enter a password"
summary: summary:
title: "Summary" title: "Summary"

View File

@ -31,10 +31,10 @@ QUnit.test("create account with user fields", async assert => {
assert.ok(exists(".create-account"), "it shows the create account modal"); assert.ok(exists(".create-account"), "it shows the create account modal");
assert.ok(exists(".user-field"), "it has at least one user field"); assert.ok(exists(".user-field"), "it has at least one user field");
assert.ok(
exists(".modal-footer .btn-primary:disabled"), await click(".modal-footer .btn-primary");
"create account is disabled at first" assert.ok(exists("#modal-alert"), "it shows the required field alert");
); assert.equal(find("#modal-alert").text(), "Please enter an email address");
await fillIn("#new-account-name", "Dr. Good Tuna"); await fillIn("#new-account-name", "Dr. Good Tuna");
await fillIn("#new-account-password", "cool password bro"); await fillIn("#new-account-password", "cool password bro");
@ -52,28 +52,13 @@ QUnit.test("create account with user fields", async assert => {
exists("#account-email-validation.good"), exists("#account-email-validation.good"),
"the email validation is good" "the email validation is good"
); );
assert.ok(
exists(".modal-footer .btn-primary:disabled"), await click(".modal-footer .btn-primary");
"create account is still disabled due to lack of user fields" assert.equal(find("#modal-alert")[0].style.display, "");
);
await fillIn(".user-field input[type=text]:first", "Barky"); await fillIn(".user-field input[type=text]:first", "Barky");
assert.ok(
exists(".modal-footer .btn-primary:disabled"),
"create account is disabled because field is not checked"
);
await click(".user-field input[type=checkbox]"); await click(".user-field input[type=checkbox]");
assert.ok( await click(".modal-footer .btn-primary");
!exists(".modal-footer .btn-primary:disabled"), assert.equal(find("#modal-alert")[0].style.display, "none");
"create account is enabled because field is checked"
);
await click(".user-field input[type=checkbox]");
assert.ok(
exists(".modal-footer .btn-primary:disabled"),
"unchecking the checkbox disables the create account button"
);
}); });

View File

@ -132,10 +132,6 @@ QUnit.test("create account", async assert => {
await click("header .sign-up-button"); await click("header .sign-up-button");
assert.ok(exists(".create-account"), "it shows the create account modal"); assert.ok(exists(".create-account"), "it shows the create account modal");
assert.ok(
exists(".modal-footer .btn-primary:disabled"),
"create account is disabled at first"
);
await fillIn("#new-account-name", "Dr. Good Tuna"); await fillIn("#new-account-name", "Dr. Good Tuna");
await fillIn("#new-account-password", "cool password bro"); await fillIn("#new-account-password", "cool password bro");
@ -151,20 +147,14 @@ QUnit.test("create account", async assert => {
exists("#username-validation.bad"), exists("#username-validation.bad"),
"the username validation is bad" "the username validation is bad"
); );
assert.ok( await click(".modal-footer .btn-primary");
exists(".modal-footer .btn-primary:disabled"), assert.ok(exists("#new-account-username:focus"), "username field is focused");
"create account is still disabled"
);
await fillIn("#new-account-username", "goodtuna"); await fillIn("#new-account-username", "goodtuna");
assert.ok( assert.ok(
exists("#username-validation.good"), exists("#username-validation.good"),
"the username validation is good" "the username validation is good"
); );
assert.not(
exists(".modal-footer .btn-primary:disabled"),
"create account is enabled"
);
await click(".modal-footer .btn-primary"); await click(".modal-footer .btn-primary");
assert.ok( assert.ok(