DEV: Replace preferences/username route with a component (#19318)
That was a weird UX (why hide the preferences navigation?) and a deprecated implementation (manually rendering a template into a named outlet) This PR replaces it with an inline component.
This commit is contained in:
parent
207b764ea3
commit
d3649873a2
@ -0,0 +1,55 @@
|
||||
{{#if this.editing}}
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
<Input
|
||||
{{on "input" this.onInput}}
|
||||
@value={{this.newUsername}}
|
||||
maxlength={{this.maxLength}}
|
||||
class="input-xxlarge username-preference__input"
|
||||
/>
|
||||
|
||||
<div class="instructions">
|
||||
<p>
|
||||
{{#if this.taken}}
|
||||
{{i18n "user.change_username.taken"}}
|
||||
{{/if}}
|
||||
<span>{{this.errorMessage}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<DButton
|
||||
@action={{this.changeUsername}}
|
||||
@type="submit"
|
||||
@disabled={{this.saveDisabled}}
|
||||
@translatedLabel={{this.saveButtonText}}
|
||||
class="btn-primary username-preference__submit"
|
||||
/>
|
||||
|
||||
<DModalCancel @close={{this.toggleEditing}} />
|
||||
|
||||
{{#if this.saved}}{{i18n "saved"}}{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="controls">
|
||||
<span class="static username-preference__current-username">{{@user.username}}</span>
|
||||
|
||||
{{#if @user.can_edit_username}}
|
||||
<DButton
|
||||
@action={{this.toggleEditing}}
|
||||
@actionParam={{@user}}
|
||||
@icon="pencil-alt"
|
||||
@title="user.username.edit"
|
||||
class="btn-small username-preference__edit-username"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if this.siteSettings.enable_mentions}}
|
||||
<div class="instructions">
|
||||
{{html-safe (i18n "user.username.short_instructions" username=@user.username)}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
@ -0,0 +1,100 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import { empty, or } from "@ember/object/computed";
|
||||
import { setting } from "discourse/lib/computed";
|
||||
import I18n from "I18n";
|
||||
import User from "discourse/models/user";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default class UsernamePreference extends Component {
|
||||
@service siteSettings;
|
||||
@service dialog;
|
||||
|
||||
@tracked editing = false;
|
||||
@tracked newUsername = this.args.user.username;
|
||||
@tracked errorMessage = null;
|
||||
@tracked saving = false;
|
||||
@tracked taken = false;
|
||||
|
||||
@setting("max_username_length") maxLength;
|
||||
@setting("min_username_length") minLength;
|
||||
@empty("newUsername") newUsernameEmpty;
|
||||
|
||||
@or("saving", "newUsernameEmpty", "taken", "unchanged", "errorMessage")
|
||||
saveDisabled;
|
||||
|
||||
get unchanged() {
|
||||
return this.newUsername === this.args.user.username;
|
||||
}
|
||||
|
||||
get saveButtonText() {
|
||||
return this.saving ? I18n.t("saving") : I18n.t("user.change");
|
||||
}
|
||||
|
||||
@action
|
||||
toggleEditing() {
|
||||
this.editing = !this.editing;
|
||||
|
||||
this.newUsername = this.args.user.username;
|
||||
this.errorMessage = null;
|
||||
this.saving = false;
|
||||
this.taken = false;
|
||||
}
|
||||
|
||||
@action
|
||||
async onInput(event) {
|
||||
this.newUsername = event.target.value;
|
||||
this.taken = false;
|
||||
this.errorMessage = null;
|
||||
|
||||
if (isEmpty(this.newUsername)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newUsername === this.args.user.username) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newUsername.length < this.minLength) {
|
||||
this.errorMessage = I18n.t("user.name.too_short");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await User.checkUsername(
|
||||
this.newUsername,
|
||||
undefined,
|
||||
this.args.user.id
|
||||
);
|
||||
|
||||
if (result.errors) {
|
||||
this.errorMessage = result.errors.join(" ");
|
||||
} else if (result.available === false) {
|
||||
this.taken = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
changeUsername() {
|
||||
return this.dialog.yesNoConfirm({
|
||||
title: I18n.t("user.change_username.confirm"),
|
||||
didConfirm: async () => {
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await this.args.user.changeUsername(this.newUsername);
|
||||
DiscourseURL.redirectTo(
|
||||
userPath(this.newUsername.toLowerCase() + "/preferences")
|
||||
);
|
||||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import { empty, or } from "@ember/object/computed";
|
||||
import { propertyEqual, setting } from "discourse/lib/computed";
|
||||
import Controller from "@ember/controller";
|
||||
import I18n from "I18n";
|
||||
import User from "discourse/models/user";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default Controller.extend({
|
||||
dialog: service(),
|
||||
taken: false,
|
||||
saving: false,
|
||||
errorMessage: null,
|
||||
newUsername: null,
|
||||
|
||||
maxLength: setting("max_username_length"),
|
||||
minLength: setting("min_username_length"),
|
||||
newUsernameEmpty: empty("newUsername"),
|
||||
saveDisabled: or(
|
||||
"saving",
|
||||
"newUsernameEmpty",
|
||||
"taken",
|
||||
"unchanged",
|
||||
"errorMessage"
|
||||
),
|
||||
unchanged: propertyEqual("newUsername", "username"),
|
||||
|
||||
@observes("newUsername")
|
||||
checkTaken() {
|
||||
let newUsername = this.newUsername;
|
||||
|
||||
if (newUsername && newUsername.length < this.minLength) {
|
||||
this.set("errorMessage", I18n.t("user.name.too_short"));
|
||||
} else {
|
||||
this.set("taken", false);
|
||||
this.set("errorMessage", null);
|
||||
|
||||
if (isEmpty(this.newUsername)) {
|
||||
return;
|
||||
}
|
||||
if (this.unchanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
User.checkUsername(newUsername, undefined, this.get("model.id")).then(
|
||||
(result) => {
|
||||
if (result.errors) {
|
||||
this.set("errorMessage", result.errors.join(" "));
|
||||
} else if (result.available === false) {
|
||||
this.set("taken", true);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("saving")
|
||||
saveButtonText(saving) {
|
||||
if (saving) {
|
||||
return I18n.t("saving");
|
||||
}
|
||||
return I18n.t("user.change");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changeUsername() {
|
||||
if (this.saveDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.dialog.yesNoConfirm({
|
||||
title: I18n.t("user.change_username.confirm"),
|
||||
didConfirm: () => {
|
||||
this.set("saving", true);
|
||||
this.model
|
||||
.changeUsername(this.newUsername)
|
||||
.then(() => {
|
||||
DiscourseURL.redirectTo(
|
||||
userPath(this.newUsername.toLowerCase() + "/preferences")
|
||||
);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set("saving", false));
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -175,7 +175,6 @@ export default function () {
|
||||
this.route("apps");
|
||||
this.route("sidebar");
|
||||
|
||||
this.route("username");
|
||||
this.route("email");
|
||||
this.route("second-factor");
|
||||
this.route("second-factor-backup");
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||
|
||||
export default RestrictedUserRoute.extend({
|
||||
showFooter: true,
|
||||
|
||||
model() {
|
||||
return this.modelFor("user");
|
||||
},
|
||||
|
||||
renderTemplate() {
|
||||
return this.render({ into: "user" });
|
||||
},
|
||||
|
||||
// A bit odd, but if we leave to /preferences we need to re-render that outlet
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
this.render("preferences", { into: "user", controller: "preferences" });
|
||||
},
|
||||
|
||||
setupController(controller, user) {
|
||||
controller.setProperties({
|
||||
model: user,
|
||||
newUsername: user.get("username"),
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -1,38 +0,0 @@
|
||||
<DSection @pageClass="user-preferences" @tagName="">
|
||||
<section class="user-preferences solo-preference">
|
||||
<form class="form-horizontal">
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<h3>{{i18n "user.change_username.title"}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="change_username" class="control-label">{{i18n "user.username.title"}}</label>
|
||||
<div class="controls">
|
||||
<TextField @value={{this.newUsername}} @id="change_username" @classNames="input-xxlarge" @maxlength={{this.maxLength}} @autofocus="autofocus" @insert-newline="changeUsername" />
|
||||
</div>
|
||||
<div class="instructions controls">
|
||||
<p>
|
||||
{{#if this.taken}}
|
||||
{{i18n "user.change_username.taken"}}
|
||||
{{/if}}
|
||||
<span>{{this.errorMessage}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<DButton @class="btn-primary" @action={{action "changeUsername"}} @type="submit" @disabled={{this.saveDisabled}} @translatedLabel={{this.saveButtonText}} />
|
||||
<LinkTo @route="preferences" @model={{this.currentUser}} class="btn btn-flat -cancel">
|
||||
{{i18n "cancel"}}
|
||||
</LinkTo>
|
||||
{{#if this.saved}}{{i18n "saved"}}{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
</DSection>
|
||||
@ -1,18 +1,6 @@
|
||||
<div class="control-group pref-username">
|
||||
<label class="control-label">{{i18n "user.username.title"}}</label>
|
||||
<div class="controls">
|
||||
<span class="static">{{this.model.username}}</span>
|
||||
{{#if this.model.can_edit_username}}
|
||||
<LinkTo @route="preferences.username" title={{i18n "user.username.edit"}} class="btn btn-default btn-small btn-icon pad-left no-text">
|
||||
{{d-icon "pencil-alt"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if this.siteSettings.enable_mentions}}
|
||||
<div class="instructions">
|
||||
{{html-safe (i18n "user.username.short_instructions" username=this.model.username)}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<UsernamePreference @user={{this.model}} />
|
||||
</div>
|
||||
|
||||
{{#unless this.siteSettings.discourse_connect_overrides_avatar}}
|
||||
|
||||
@ -111,11 +111,6 @@ acceptance("User Preferences", function (needs) {
|
||||
"apps tab isn't there when you have no authorized apps"
|
||||
);
|
||||
});
|
||||
|
||||
test("username", async function (assert) {
|
||||
await visit("/u/eviltrout/preferences/username");
|
||||
assert.ok(exists("#change_username"), "it has the input element");
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Custom User Fields", function (needs) {
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { test } from "qunit";
|
||||
import I18n from "I18n";
|
||||
import sinon from "sinon";
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
query,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { fixturesByUrl } from "discourse/tests/helpers/create-pretender";
|
||||
import pretender, {
|
||||
fixturesByUrl,
|
||||
response,
|
||||
} from "discourse/tests/helpers/create-pretender";
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
|
||||
acceptance("User Preferences - Account", function (needs) {
|
||||
@ -58,6 +61,41 @@ acceptance("User Preferences - Account", function (needs) {
|
||||
pickAvatarRequestData = null;
|
||||
});
|
||||
|
||||
test("changing username", async function (assert) {
|
||||
const stub = sinon
|
||||
.stub(DiscourseURL, "redirectTo")
|
||||
.withArgs("/u/good_trout/preferences");
|
||||
|
||||
pretender.put("/u/eviltrout/preferences/username", (data) => {
|
||||
assert.strictEqual(data.requestBody, "new_username=good_trout");
|
||||
|
||||
return response({
|
||||
id: fixturesByUrl["/u/eviltrout.json"].user.id,
|
||||
username: "good_trout",
|
||||
});
|
||||
});
|
||||
|
||||
await visit("/u/eviltrout/preferences/account");
|
||||
|
||||
assert.strictEqual(
|
||||
query(".username-preference__current-username").innerText,
|
||||
"eviltrout"
|
||||
);
|
||||
|
||||
await click(".username-preference__edit-username");
|
||||
|
||||
assert.strictEqual(query(".username-preference__input").value, "eviltrout");
|
||||
assert.true(query(".username-preference__submit").disabled);
|
||||
|
||||
await fillIn(query(".username-preference__input"), "good_trout");
|
||||
assert.false(query(".username-preference__submit").disabled);
|
||||
|
||||
await click(".username-preference__submit");
|
||||
await click(".dialog-container .btn-primary");
|
||||
|
||||
sinon.assert.calledOnce(stub);
|
||||
});
|
||||
|
||||
test("Delete dialog", async function (assert) {
|
||||
sinon.stub(DiscourseURL, "redirectAbsolute");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user