DEV: replace UserFieldsValidation mixin with helper class (#31670)

This PR refactors the UserFieldsValidation mixin into a helper class.

### Key Changes
* moved this.userFields to a tracked collection of TrackedUserFields
stored on the helper class itself
* introduced `validationVisible` property to explicitly control when to
display the validation result, we expect the helper to not display
validation until form submission in the `SignupController` and
`CreateAccount` components.
* added backward compatibility to the plugin API
`addCustomUserFieldValidationCallback` to ensure ex-Core instances of
the mixin continues working till we remove them
This commit is contained in:
Kelv
2025-03-12 20:50:53 +08:00
committed by GitHub
parent b56816ad70
commit 1cb940b113
5 changed files with 175 additions and 26 deletions

View File

@ -36,16 +36,14 @@ import discourseComputed, { bind } from "discourse/lib/decorators";
import NameValidationHelper from "discourse/lib/name-validation-helper";
import PasswordValidationHelper from "discourse/lib/password-validation-helper";
import { userPath } from "discourse/lib/url";
import UserFieldsValidationHelper from "discourse/lib/user-fields-validation-helper";
import UsernameValidationHelper from "discourse/lib/username-validation-helper";
import { emailValid } from "discourse/lib/utilities";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
import { findAll } from "discourse/models/login-method";
import User from "discourse/models/user";
import { i18n } from "discourse-i18n";
export default class CreateAccount extends Component.extend(
UserFieldsValidation
) {
export default class CreateAccount extends Component {
@service site;
@service siteSettings;
@service login;
@ -62,12 +60,16 @@ export default class CreateAccount extends Component.extend(
formSubmitted = false;
rejectedEmails = A();
prefilledUsername = null;
userFields = null;
maskPassword = true;
emailValidationVisible = false;
nameValidationHelper = new NameValidationHelper(this);
usernameValidationHelper = new UsernameValidationHelper(this);
passwordValidationHelper = new PasswordValidationHelper(this);
userFieldsValidationHelper = new UserFieldsValidationHelper({
getUserFields: () => this.site.get("user_fields"),
getAccountPassword: () => this.accountPassword,
showValidationOnInit: false,
});
@setting("enable_local_logins") canCreateLocal;
@setting("require_invite_code") requireInviteCode;
@ -88,6 +90,16 @@ export default class CreateAccount extends Component.extend(
}
}
@dependentKeyCompat
get userFields() {
return this.userFieldsValidationHelper.userFields;
}
@dependentKeyCompat
get userFieldsValidation() {
return this.userFieldsValidationHelper.userFieldsValidation;
}
@action
setAccountUsername(event) {
this.accountUsername = event.target.value;
@ -436,9 +448,7 @@ export default class CreateAccount extends Component.extend(
// Add the userFields to the data
if (!isEmpty(this.userFields)) {
attrs.userFields = {};
this.userFields.forEach(
(f) => (attrs.userFields[f.get("field.id")] = f.get("value"))
);
this.userFields.forEach((f) => (attrs.userFields[f.field.id] = f.value));
}
this.set("formSubmitted", true);
@ -529,6 +539,7 @@ export default class CreateAccount extends Component.extend(
createAccount() {
this.set("flash", "");
this.nameValidationHelper.forceValidationReason = true;
this.userFieldsValidationHelper.validationVisible = true;
this.set("emailValidationVisible", true);
const validation = [
@ -555,6 +566,7 @@ export default class CreateAccount extends Component.extend(
return;
}
this.userFieldsValidationHelper.validationVisible = false;
this.nameValidationHelper.forceValidationReason = false;
this.performAccountCreation();
}

View File

@ -11,15 +11,13 @@ import getUrl from "discourse/lib/get-url";
import NameValidationHelper from "discourse/lib/name-validation-helper";
import PasswordValidationHelper from "discourse/lib/password-validation-helper";
import DiscourseURL from "discourse/lib/url";
import UserFieldsValidationHelper from "discourse/lib/user-fields-validation-helper";
import UsernameValidationHelper from "discourse/lib/username-validation-helper";
import { emailValid } from "discourse/lib/utilities";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
import { findAll as findLoginMethods } from "discourse/models/login-method";
import { i18n } from "discourse-i18n";
export default class InvitesShowController extends Controller.extend(
UserFieldsValidation
) {
export default class InvitesShowController extends Controller {
@tracked accountPassword;
@tracked accountUsername;
@tracked isDeveloper;
@ -27,6 +25,10 @@ export default class InvitesShowController extends Controller.extend(
nameValidationHelper = new NameValidationHelper(this);
usernameValidationHelper = new UsernameValidationHelper(this);
passwordValidationHelper = new PasswordValidationHelper(this);
userFieldsValidationHelper = new UserFieldsValidationHelper({
getUserFields: () => this.site.get("user_fields"),
getAccountPassword: () => this.accountPassword,
});
successMessage = null;
@readOnly("model.is_invite_link") isInviteLink;
@readOnly("model.invited_by") invitedBy;
@ -41,11 +43,19 @@ export default class InvitesShowController extends Controller.extend(
@alias("model.different_external_email") differentExternalEmail;
@not("externalAuthsOnly") passwordRequired;
errorMessage = null;
userFields = null;
authOptions = null;
rejectedEmails = [];
maskPassword = true;
get userFields() {
return this.userFieldsValidationHelper.userFields;
}
@dependentKeyCompat
get userFieldsValidation() {
return this.userFieldsValidationHelper.userFieldsValidation;
}
@action
setAccountUsername(event) {
this.accountUsername = event.target.value;
@ -320,11 +330,10 @@ export default class InvitesShowController extends Controller.extend(
@action
submit() {
const userFields = this.userFields;
let userCustomFields = {};
if (!isEmpty(userFields)) {
userFields.forEach(function (f) {
userCustomFields[f.get("field.id")] = f.get("value");
if (!isEmpty(this.userFields)) {
this.userFields.forEach(function (f) {
userCustomFields[f.field.id] = f.value;
});
}

View File

@ -16,16 +16,14 @@ import discourseComputed, { bind } from "discourse/lib/decorators";
import NameValidationHelper from "discourse/lib/name-validation-helper";
import PasswordValidationHelper from "discourse/lib/password-validation-helper";
import { userPath } from "discourse/lib/url";
import UserFieldsValidationHelper from "discourse/lib/user-fields-validation-helper";
import UsernameValidationHelper from "discourse/lib/username-validation-helper";
import { emailValid } from "discourse/lib/utilities";
import UserFieldsValidation from "discourse/mixins/user-fields-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(
UserFieldsValidation
) {
export default class SignupPageController extends Controller {
@service site;
@service siteSettings;
@service login;
@ -42,12 +40,16 @@ export default class SignupPageController extends Controller.extend(
formSubmitted = false;
rejectedEmails = A();
prefilledUsername = null;
userFields = null;
maskPassword = true;
emailValidationVisible = false;
nameValidationHelper = new NameValidationHelper(this);
usernameValidationHelper = new UsernameValidationHelper(this);
passwordValidationHelper = new PasswordValidationHelper(this);
userFieldsValidationHelper = new UserFieldsValidationHelper({
getUserFields: () => this.site.get("user_fields"),
getAccountPassword: () => this.accountPassword,
showValidationOnInit: false,
});
@notEmpty("authOptions") hasAuthOptions;
@setting("enable_local_logins") canCreateLocal;
@ -63,6 +65,16 @@ export default class SignupPageController extends Controller.extend(
this.fetchConfirmationValue();
}
@dependentKeyCompat
get userFields() {
return this.userFieldsValidationHelper.userFields;
}
@dependentKeyCompat
get userFieldsValidation() {
return this.userFieldsValidationHelper.userFieldsValidation;
}
@dependentKeyCompat
get usernameValidation() {
return this.usernameValidationHelper.usernameValidation;
@ -421,9 +433,7 @@ export default class SignupPageController extends Controller.extend(
// Add the userFields to the data
if (!isEmpty(this.userFields)) {
attrs.userFields = {};
this.userFields.forEach(
(f) => (attrs.userFields[f.get("field.id")] = f.get("value"))
);
this.userFields.forEach((f) => (attrs.userFields[f.field.id] = f.value));
}
this.set("formSubmitted", true);
@ -514,6 +524,7 @@ export default class SignupPageController extends Controller.extend(
createAccount() {
this.set("flash", "");
this.nameValidationHelper.forceValidationReason = true;
this.userFieldsValidationHelper.validationVisible = true;
this.set("emailValidationVisible", true);
const validation = [
@ -540,6 +551,7 @@ export default class SignupPageController extends Controller.extend(
return;
}
this.userFieldsValidationHelper.validationVisible = false;
this.nameValidationHelper.forceValidationReason = false;
this.performAccountCreation();
}

View File

@ -117,9 +117,10 @@ import {
_registerTransformer,
transformerTypes,
} from "discourse/lib/transformer";
import { addCustomUserFieldValidationCallback } from "discourse/lib/user-fields-validation-helper";
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
import { replaceFormatter } from "discourse/lib/utilities";
import { addCustomUserFieldValidationCallback } from "discourse/mixins/user-fields-validation";
import { addCustomUserFieldValidationCallback as addCustomUserFieldValidationCallbackDeprecated } from "discourse/mixins/user-fields-validation";
import Composer, {
registerCustomizationCallback,
} from "discourse/models/composer";
@ -1572,6 +1573,7 @@ class PluginApi {
**/
addCustomUserFieldValidationCallback(callback) {
addCustomUserFieldValidationCallbackDeprecated(callback);
addCustomUserFieldValidationCallback(callback);
}

View File

@ -0,0 +1,114 @@
import { tracked } from "@glimmer/tracking";
import { isEmpty } from "@ember/utils";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import { i18n } from "discourse-i18n";
const addCustomUserFieldValidationCallbacks = [];
export function addCustomUserFieldValidationCallback(callback) {
addCustomUserFieldValidationCallbacks.push(callback);
}
function failedResult(attrs) {
return {
failed: true,
ok: false,
...attrs,
};
}
function validResult(attrs) {
return { ok: true, ...attrs };
}
class TrackedUserField {
@tracked value = null;
field;
getValidationVisible;
getAccountPassword;
constructor({ field, getValidationVisible, getAccountPassword }) {
this.field = field;
this.getValidationVisible = getValidationVisible;
this.getAccountPassword = getAccountPassword;
}
get validation() {
if (!this.getValidationVisible()) {
return validResult();
}
let validation = validResult();
if (this.field.required && (!this.value || isEmpty(this.value))) {
const reasonKey =
this.field.field_type === "confirm"
? "user_fields.required_checkbox"
: "user_fields.required";
validation = failedResult({
reason: i18n(reasonKey, {
name: this.field.name,
}),
element: this.field.element,
});
} else if (
this.getAccountPassword() &&
this.field.field_type === "text" &&
this.value &&
this.value.toLowerCase().includes(this.getAccountPassword().toLowerCase())
) {
validation = failedResult({
reason: i18n("user_fields.same_as_password"),
element: this.field.element,
});
}
addCustomUserFieldValidationCallbacks.forEach((callback) => {
const customUserFieldValidationObject = callback(this);
if (customUserFieldValidationObject) {
validation = customUserFieldValidationObject;
}
});
return validation;
}
}
export default class UserFieldsValidationHelper {
@tracked userFields = new TrackedArray();
@tracked validationVisible = true;
constructor({
getUserFields,
getAccountPassword,
showValidationOnInit = true,
}) {
this.getUserFields = getUserFields;
this.getAccountPassword = getAccountPassword;
this.validationVisible = showValidationOnInit;
this.initializeUserFields();
}
initializeUserFields() {
let userFields = this.getUserFields();
if (userFields) {
const getValidationVisible = () => this.validationVisible;
this.userFields = new TrackedArray(
userFields.sortBy("position").map((f) => {
return new TrackedUserField({
field: f,
getValidationVisible,
getAccountPassword: this.getAccountPassword,
});
})
);
}
}
get userFieldsValidation() {
if (!this.userFields) {
return validResult();
}
const invalidUserField = this.userFields.find((f) => f.validation.failed);
return invalidUserField ? invalidUserField.validation : validResult();
}
}