From 80fdb6f2e671761e78a595dd50c04043c62e5552 Mon Sep 17 00:00:00 2001 From: Kelv Date: Mon, 3 Feb 2025 22:27:45 +0800 Subject: [PATCH] DEV: refactor username validation mixin to helper class (#31107) This PR refactors the use of the UsernameValidation mixin to a helper class for the SignupController component. We'll extend this to the CreateAccount modal and InvitesShowController in follow-up PRs. --- .../discourse/app/controllers/signup.js | 15 ++- .../app/lib/username-validation-helper.js | 116 ++++++++++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/lib/username-validation-helper.js diff --git a/app/assets/javascripts/discourse/app/controllers/signup.js b/app/assets/javascripts/discourse/app/controllers/signup.js index 6a98f5f1c6f..0754a08bc6b 100644 --- a/app/assets/javascripts/discourse/app/controllers/signup.js +++ b/app/assets/javascripts/discourse/app/controllers/signup.js @@ -15,17 +15,16 @@ import discourseDebounce from "discourse/lib/debounce"; import discourseComputed, { bind } from "discourse/lib/decorators"; import NameValidationHelper from "discourse/lib/name-validation-helper"; import { userPath } from "discourse/lib/url"; +import UsernameValidationHelper from "discourse/lib/username-validation-helper"; import { emailValid } from "discourse/lib/utilities"; import PasswordValidation from "discourse/mixins/password-validation"; import UserFieldsValidation from "discourse/mixins/user-fields-validation"; -import UsernameValidation from "discourse/mixins/username-validation"; import { findAll } from "discourse/models/login-method"; import User from "discourse/models/user"; import { i18n } from "discourse-i18n"; export default class SignupPageController extends Controller.extend( PasswordValidation, - UsernameValidation, UserFieldsValidation ) { @service site; @@ -44,6 +43,7 @@ export default class SignupPageController extends Controller.extend( passwordValidationVisible = false; emailValidationVisible = false; nameValidationHelper = new NameValidationHelper(this); + usernameValidationHelper = new UsernameValidationHelper(this); @notEmpty("authOptions") hasAuthOptions; @setting("enable_local_logins") canCreateLocal; @@ -59,6 +59,11 @@ export default class SignupPageController extends Controller.extend( this.fetchConfirmationValue(); } + @dependentKeyCompat + get usernameValidation() { + return this.usernameValidationHelper.usernameValidation; + } + get nameTitle() { return this.nameValidationHelper.nameTitle; } @@ -356,7 +361,11 @@ export default class SignupPageController extends Controller.extend( // If email is valid and username has not been entered yet, // or email and username were filled automatically by 3rd party auth, // then look for a registered username that matches the email. - discourseDebounce(this, this.fetchExistingUsername, 500); + discourseDebounce( + this, + this.usernameValidationHelper.fetchExistingUsername, + 500 + ); } } diff --git a/app/assets/javascripts/discourse/app/lib/username-validation-helper.js b/app/assets/javascripts/discourse/app/lib/username-validation-helper.js new file mode 100644 index 00000000000..9ee9fd0da2e --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/username-validation-helper.js @@ -0,0 +1,116 @@ +import { tracked } from "@glimmer/tracking"; +import { isEmpty } from "@ember/utils"; +import discourseDebounce from "discourse/lib/debounce"; +import User from "discourse/models/user"; +import { i18n } from "discourse-i18n"; + +function failedResult(attrs) { + return { + shouldCheck: false, + failed: true, + ok: false, + element: document.querySelector("#new-account-username"), + ...attrs, + }; +} + +function validResult(attrs) { + return { ok: true, ...attrs }; +} + +export default class UsernameValidationHelper { + @tracked usernameValidationResult; + checkedUsername = null; + + constructor(owner) { + this.owner = owner; + } + + async fetchExistingUsername() { + const result = await User.checkUsername(null, this.owner.accountEmail); + + if ( + result.suggestion && + (isEmpty(this.owner.accountUsername) || + this.owner.accountUsername === this.owner.get("authOptions.username")) + ) { + this.owner.accountUsername = result.suggestion; + this.owner.prefilledUsername = result.suggestion; + } + } + + get usernameValidation() { + if ( + this.usernameValidationResult && + this.checkedUsername === this.owner.accountUsername + ) { + return this.usernameValidationResult; + } + + const result = this.basicUsernameValidation(this.owner.accountUsername); + + if (result.shouldCheck) { + discourseDebounce(this, this.checkUsernameAvailability, 500); + } + + return result; + } + + basicUsernameValidation(username) { + if (username && username === this.owner.prefilledUsername) { + return validResult({ reason: i18n("user.username.prefilled") }); + } + + if (isEmpty(username)) { + return failedResult({ + message: i18n("user.username.required"), + reason: this.owner.forceValidationReason + ? i18n("user.username.required") + : null, + }); + } + + if (username.length < this.owner.siteSettings.min_username_length) { + return failedResult({ reason: i18n("user.username.too_short") }); + } + + if (username.length > this.owner.siteSettings.max_username_length) { + return failedResult({ reason: i18n("user.username.too_long") }); + } + + return failedResult({ + shouldCheck: true, + reason: i18n("user.username.checking"), + }); + } + + async checkUsernameAvailability() { + const result = await User.checkUsername( + this.owner.accountUsername, + this.owner.accountEmail + ); + + if (this.owner.isDestroying || this.owner.isDestroyed) { + return; + } + + this.checkedUsername = this.owner.accountUsername; + this.owner.isDeveloper = !!result.is_developer; + + if (result.available) { + this.usernameValidationResult = validResult({ + reason: i18n("user.username.available"), + }); + } else if (result.suggestion) { + this.usernameValidationResult = failedResult({ + reason: i18n("user.username.not_available", result), + }); + } else { + this.usernameValidationResult = failedResult({ + reason: result.errors + ? result.errors.join(" ") + : i18n("user.username.not_available_no_suggestion"), + }); + } + } +}