Migrated staff user screen to Ember Octane patterns (#15532)
refs https://github.com/TryGhost/Ghost/issues/14101 - migrated staff user controller to native class syntax - removed use of `{{action}}` helper - moved from custom components to native `<input>` and `<textarea>` for form fields - added `{{select-on-click}}` modifier to cover the `<GhTextingInput @selectOnClick>` option behaviour for any input element - added `submitForm()` test helper that finds closest `form` element and trigger's a `submit` event on it simulating <kbd>Enter</kbd> being pressed whilst a field has focus
This commit is contained in:
parent
b96ff6ae4a
commit
524b23c182
@ -901,3 +901,33 @@ remove|ember-template-lint|no-action|280|43|280|43|d746c5532ce38d23d8537195a3afa
|
||||
remove|ember-template-lint|no-action|398|106|398|106|d2028f094665a3702bbea21159b1c12237c94bc9|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|262|32|262|32|c14e20b2b090706f9af9fa416c4300c06e206894|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|279|32|279|32|e286f50c92a17a31581207e4c65054f146cbd533|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|155|39|155|39|3da2270da5495c84c2b069564f2163b109d0e73e|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|156|43|156|43|4cd04fee6bbe85afe19501cd2f393c567e2f465e|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|175|39|175|39|66cebfc8448eced0bccf3faa57b4af0cd633e65e|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|176|43|176|43|3a3cdadac5b48ea65a6b10f0bb9319bed6d9ed79|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|196|43|196|43|eaee037b0dc6f3a8903f73c20098dfd77cba770c|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|197|47|197|47|b435e7cd7f48e3edb3214adc9c171181805500e3|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|230|39|230|39|5340ffcfe14bc0696c786cb698dc7436ff3e7b2a|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|231|43|231|43|cbc552ad67fd7382c02ac946a3de0fbe7dbb8509|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|246|39|246|39|6c405ec87c2cbc8470edc3bcba2ab93bf20e5c17|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|247|43|247|43|b26a1d1b4ba0bbe491099aeb78875160e2e417d6|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|292|39|292|39|61d6401bd1e43f082ab6fa49c83002d2e3faa886|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|293|43|293|43|eb4de7d8a9578a4b960da7d9b7679f3b1d67f886|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|409|47|409|47|4661ca0fecfa23048521845c78a5a7cfce28609f|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|411|50|411|50|6403bc3b28507827968bd1fcd428d36d63b53511|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|426|43|426|43|c262ddbe8aa83b429a32392155869d7a068a5c3e|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|428|46|428|46|6403bc3b28507827968bd1fcd428d36d63b53511|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|441|43|441|43|08c167af90156f5bce0a15fe0153ba0c222297b6|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|443|46|443|46|6403bc3b28507827968bd1fcd428d36d63b53511|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-action|476|62|476|62|38bfdf3e4ec8c79d195e80a48c2f343757845cab|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|155|32|155|32|7f1d3292c273e623c95015be04a3fc5bac131480|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|175|32|175|32|fed655605208b290a18b9f7da51a6cf7b40c0e9a|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|196|36|196|36|881cf5dcdbe984cb7b4d5a2ddde4e111147b3817|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|230|32|230|32|23912c3130690f1fb910732f42bc87922e3bc317|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|246|32|246|32|50e244fdb1572202192f659fe4052f18bea92552|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|292|32|292|32|b6eefc591c761dcae13b0de50e074f5aac8f1d80|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|409|40|409|40|f9ad8f1af9275247b94cbab1de26e9f9d2218da5|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|426|36|426|36|2a5f6ae5ef152f0c0d953cfbaf810fdf0d08e996|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-passed-in-event-handlers|441|36|441|36|c9d092bb6707073e3f11bdc393d1c1361e5f87c4|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-potential-path-strings|470|53|470|53|5c3a010b4844cd530c312ef2aa32de64b8a43db1|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
remove|ember-template-lint|no-valueless-arguments|468|44|468|44|5d042f1f94738e82cc1b0c5c008cece383beaa30|1662681600000|1673053200000|1678237200000|app/templates/settings/staff/user.hbs
|
||||
|
@ -6,65 +6,81 @@ import SuspendUserModal from '../../../components/settings/staff/modals/suspend-
|
||||
import TransferOwnershipModal from '../../../components/settings/staff/modals/transfer-ownership';
|
||||
import UnsuspendUserModal from '../../../components/settings/staff/modals/unsuspend-user';
|
||||
import UploadImageModal from '../../../components/settings/staff/modals/upload-image';
|
||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
|
||||
import isNumber from 'ghost-admin/utils/isNumber';
|
||||
import windowProxy from 'ghost-admin/utils/window-proxy';
|
||||
import {TrackedObject} from 'tracked-built-ins';
|
||||
import {action, computed} from '@ember/object';
|
||||
import {alias, and, not, or, readOnly} from '@ember/object/computed';
|
||||
import {action} from '@ember/object';
|
||||
import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task, taskGroup, timeout} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default Controller.extend({
|
||||
ajax: service(),
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
membersUtils: service(),
|
||||
modals: service(),
|
||||
notifications: service(),
|
||||
session: service(),
|
||||
slugGenerator: service(),
|
||||
utils: service(),
|
||||
export default class UserController extends Controller {
|
||||
@service ajax;
|
||||
@service config;
|
||||
@service ghostPaths;
|
||||
@service membersUtils;
|
||||
@service modals;
|
||||
@service notifications;
|
||||
@service session;
|
||||
@service slugGenerator;
|
||||
@service utils;
|
||||
|
||||
personalToken: null,
|
||||
personalTokenRegenerated: false,
|
||||
dirtyAttributes: false,
|
||||
@tracked dirtyAttributes = false;
|
||||
@tracked personalToken = null;
|
||||
@tracked personalTokenRegenerated = false;
|
||||
@tracked scratchValues = new TrackedObject();
|
||||
@tracked slugValue = null; // not set directly on model to avoid URL changing before save
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.clearScratchValues();
|
||||
},
|
||||
get user() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
scratchValues: tracked(),
|
||||
get currentUser() {
|
||||
return this.session.user;
|
||||
}
|
||||
|
||||
saveHandlers: taskGroup().enqueue(),
|
||||
get isOwnProfile() {
|
||||
return this.currentUser.id === this.user.id;
|
||||
}
|
||||
|
||||
user: alias('model'),
|
||||
currentUser: alias('session.user'),
|
||||
get isAdminUserOnOwnProfile() {
|
||||
return this.currentUser.isAdminOnly && this.isOwnProfile;
|
||||
}
|
||||
|
||||
email: readOnly('user.email'),
|
||||
slugValue: boundOneWay('user.slug'),
|
||||
get isAdminUserOnOwnerProfile() {
|
||||
return this.currentUser.isAdminOnly && this.user.isOwnerOnly;
|
||||
}
|
||||
|
||||
canChangeEmail: not('isAdminUserOnOwnerProfile'),
|
||||
canChangePassword: not('isAdminUserOnOwnerProfile'),
|
||||
canToggleMemberAlerts: or('currentUser.isOwnerOnly', 'isAdminUserOnOwnProfile'),
|
||||
isAdminUserOnOwnProfile: and('currentUser.isAdminOnly', 'isOwnProfile'),
|
||||
canMakeOwner: and('currentUser.isOwnerOnly', 'isNotOwnProfile', 'user.isAdminOnly', 'isNotSuspended'),
|
||||
isAdminUserOnOwnerProfile: and('currentUser.isAdminOnly', 'user.isOwnerOnly'),
|
||||
isNotOwnersProfile: not('user.isOwnerOnly'),
|
||||
isNotSuspended: not('user.isSuspended'),
|
||||
rolesDropdownIsVisible: and('currentUser.isAdmin', 'isNotOwnProfile', 'isNotOwnersProfile'),
|
||||
userActionsAreVisible: or('deleteUserActionIsVisible', 'canMakeOwner'),
|
||||
get canChangeEmail() {
|
||||
return !this.isAdminUserOnOwnerProfile;
|
||||
}
|
||||
|
||||
isNotOwnProfile: not('isOwnProfile'),
|
||||
isOwnProfile: computed('user.id', 'currentUser.id', function () {
|
||||
return this.get('user.id') === this.get('currentUser.id');
|
||||
}),
|
||||
get canChangePassword() {
|
||||
return !this.isAdminUserOnOwnerProfile;
|
||||
}
|
||||
|
||||
deleteUserActionIsVisible: computed('currentUser.{isAdmin,isEditor}', 'user.{isOwnerOnly,isAuthorOrContributor}', 'isOwnProfile', function () {
|
||||
get canMakeOwner() {
|
||||
return this.currentUser.isOwnerOnly
|
||||
&& !this.isOwnProfile
|
||||
&& this.user.isAdminOnly
|
||||
&& !this.user.isSuspended;
|
||||
}
|
||||
|
||||
get canToggleMemberAlerts() {
|
||||
return this.currentUser.isOwnerOnly || this.isAdminUserOnOwnProfile;
|
||||
}
|
||||
|
||||
get rolesDropdownIsVisible() {
|
||||
return this.currentUser.isAdmin && !this.isOwnProfile && !this.user.isOwnerOnly;
|
||||
}
|
||||
|
||||
get userActionsAreVisible() {
|
||||
return this.deleteUserActionIsVisible || this.canMakeOwner;
|
||||
}
|
||||
|
||||
get deleteUserActionIsVisible() {
|
||||
// users can't delete themselves
|
||||
if (this.isOwnProfile) {
|
||||
return false;
|
||||
@ -72,137 +88,129 @@ export default Controller.extend({
|
||||
|
||||
if (
|
||||
// owners/admins can delete any non-owner user
|
||||
(this.currentUser.get('isAdmin') && !this.user.isOwnerOnly) ||
|
||||
(this.currentUser.isAdmin && !this.user.isOwnerOnly) ||
|
||||
// editors can delete any author or contributor
|
||||
(this.currentUser.get('isEditor') && this.user.isAuthorOrContributor)
|
||||
(this.currentUser.isEditor && this.user.isAuthorOrContributor)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
}
|
||||
|
||||
coverTitle: computed('user.name', function () {
|
||||
return `${this.get('user.name')}'s Cover Image`;
|
||||
}),
|
||||
@action
|
||||
setModelProperty(property, event) {
|
||||
const value = event.target.value;
|
||||
this.user[property] = value;
|
||||
}
|
||||
|
||||
roles: computed(function () {
|
||||
return this.store.query('role', {permissions: 'assign'});
|
||||
}),
|
||||
@action
|
||||
validateModelProperty(property) {
|
||||
this.user.validate({property});
|
||||
}
|
||||
|
||||
actions: {
|
||||
// TODO: remove those mutation actions once we have better
|
||||
// inline validations that auto-clear errors on input
|
||||
updatePassword(password) {
|
||||
this.set('user.password', password);
|
||||
this.get('user.hasValidated').removeObject('password');
|
||||
this.get('user.errors').remove('password');
|
||||
},
|
||||
@action
|
||||
clearModelErrors(property) {
|
||||
this.user.hasValidated.removeObject(property);
|
||||
this.user.errors.remove(property);
|
||||
}
|
||||
|
||||
updateNewPassword(password) {
|
||||
this.set('user.newPassword', password);
|
||||
this.get('user.hasValidated').removeObject('newPassword');
|
||||
this.get('user.errors').remove('newPassword');
|
||||
},
|
||||
@action
|
||||
setSlugValue(event) {
|
||||
this.slugValue = event.target.value;
|
||||
}
|
||||
|
||||
updateNe2Password(password) {
|
||||
this.set('user.ne2Password', password);
|
||||
this.get('user.hasValidated').removeObject('ne2Password');
|
||||
this.get('user.errors').remove('ne2Password');
|
||||
}
|
||||
},
|
||||
@action
|
||||
async deleteUser() {
|
||||
await this.modals.open(DeleteUserModal, {
|
||||
user: this.model
|
||||
});
|
||||
}
|
||||
|
||||
deleteUser: action(async function () {
|
||||
if (this.deleteUserActionIsVisible) {
|
||||
await this.modals.open(DeleteUserModal, {
|
||||
user: this.model
|
||||
});
|
||||
}
|
||||
}),
|
||||
@action
|
||||
async suspendUser() {
|
||||
await this.modals.open(SuspendUserModal, {
|
||||
user: this.model,
|
||||
saveTask: this.saveTask
|
||||
});
|
||||
}
|
||||
|
||||
suspendUser: action(async function () {
|
||||
if (this.deleteUserActionIsVisible) {
|
||||
await this.modals.open(SuspendUserModal, {
|
||||
user: this.model,
|
||||
saveTask: this.save
|
||||
});
|
||||
}
|
||||
}),
|
||||
@action
|
||||
async unsuspendUser() {
|
||||
await this.modals.open(UnsuspendUserModal, {
|
||||
user: this.model,
|
||||
saveTask: this.saveTask
|
||||
});
|
||||
}
|
||||
|
||||
unsuspendUser: action(async function () {
|
||||
if (this.deleteUserActionIsVisible) {
|
||||
await this.modals.open(UnsuspendUserModal, {
|
||||
user: this.model,
|
||||
saveTask: this.save
|
||||
});
|
||||
}
|
||||
}),
|
||||
@action
|
||||
async transferOwnership() {
|
||||
await this.modals.open(TransferOwnershipModal, {
|
||||
user: this.model
|
||||
});
|
||||
}
|
||||
|
||||
transferOwnership: action(async function () {
|
||||
if (this.canMakeOwner) {
|
||||
await this.modals.open(TransferOwnershipModal, {
|
||||
user: this.model
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
regenerateStaffToken: action(async function () {
|
||||
@action
|
||||
async regenerateStaffToken() {
|
||||
const apiToken = await this.modals.open(RegenerateStaffTokenModal);
|
||||
|
||||
if (apiToken) {
|
||||
this.set('personalToken', apiToken);
|
||||
this.set('personalTokenRegenerated', true);
|
||||
this.personalToken = apiToken;
|
||||
this.personalTokenRegenerated = true;
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
selectRole: action(async function () {
|
||||
@action
|
||||
async selectRole() {
|
||||
const newRole = await this.modals.open(SelectRoleModal, {
|
||||
currentRole: this.model.role
|
||||
});
|
||||
|
||||
if (newRole) {
|
||||
this.user.role = newRole;
|
||||
this.set('dirtyAttributes', true);
|
||||
this.dirtyAttributes = true;
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
changeCoverImage: action(async function () {
|
||||
@action
|
||||
async changeCoverImage() {
|
||||
await this.modals.open(UploadImageModal, {
|
||||
model: this.model,
|
||||
modelProperty: 'coverImage'
|
||||
});
|
||||
}),
|
||||
}
|
||||
|
||||
changeProfileImage: action(async function () {
|
||||
@action
|
||||
async changeProfileImage() {
|
||||
await this.modals.open(UploadImageModal, {
|
||||
model: this.model,
|
||||
modelProperty: 'profileImage'
|
||||
});
|
||||
}),
|
||||
}
|
||||
|
||||
setScratchValue: action(function (property, value) {
|
||||
@action
|
||||
setScratchValue(property, value) {
|
||||
this.scratchValues[property] = value;
|
||||
}),
|
||||
}
|
||||
|
||||
clearScratchValues() {
|
||||
this.scratchValues = new TrackedObject();
|
||||
},
|
||||
|
||||
reset: action(function () {
|
||||
@action
|
||||
reset() {
|
||||
this.user.rollbackAttributes();
|
||||
this.user.password = '';
|
||||
this.user.newPassword = '';
|
||||
this.user.ne2Password = '';
|
||||
this.set('slugValue', this.user.slug);
|
||||
this.set('dirtyAttributes', false);
|
||||
this.slugValue = this.user.slug;
|
||||
this.dirtyAttributes = false;
|
||||
this.clearScratchValues();
|
||||
}),
|
||||
}
|
||||
|
||||
toggleCommentNotifications: action(function (event) {
|
||||
@action
|
||||
toggleCommentNotifications(event) {
|
||||
this.user.commentNotifications = event.target.checked;
|
||||
}),
|
||||
}
|
||||
|
||||
toggleMemberEmailAlerts: action(function (type, event) {
|
||||
@action
|
||||
toggleMemberEmailAlerts(type, event) {
|
||||
if (type === 'free-signup') {
|
||||
this.user.freeMemberSignupNotification = event.target.checked;
|
||||
} else if (type === 'paid-started') {
|
||||
@ -210,17 +218,21 @@ export default Controller.extend({
|
||||
} else if (type === 'paid-canceled') {
|
||||
this.user.paidSubscriptionCanceledNotification = event.target.checked;
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
updateSlug: task(function* (newSlug) {
|
||||
let slug = this.get('user.slug');
|
||||
@taskGroup({enqueue: true}) saveHandlers;
|
||||
|
||||
@task({group: 'saveHandlers'})
|
||||
*updateSlugTask(event) {
|
||||
let newSlug = event.target.value;
|
||||
let slug = this.user.slug;
|
||||
|
||||
newSlug = newSlug || slug;
|
||||
newSlug = newSlug.trim();
|
||||
|
||||
// Ignore unchanged slugs or candidate slugs that are empty
|
||||
if (!newSlug || slug === newSlug) {
|
||||
this.set('slugValue', slug);
|
||||
this.slugValue = slug;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -247,26 +259,27 @@ export default Controller.extend({
|
||||
// for the incrementor then the existing slug should be used
|
||||
if (isNumber(check) && check > 0) {
|
||||
if (slug === slugTokens.join('-') && serverSlug !== newSlug) {
|
||||
this.set('slugValue', slug);
|
||||
this.slugValue = slug;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.set('slugValue', serverSlug);
|
||||
this.set('dirtyAttributes', true);
|
||||
this.slugValue = serverSlug;
|
||||
this.dirtyAttributes = true;
|
||||
|
||||
return true;
|
||||
}).group('saveHandlers'),
|
||||
}
|
||||
|
||||
save: task(function* () {
|
||||
@task({group: 'saveHandlers'})
|
||||
*saveTask() {
|
||||
let user = this.user;
|
||||
let slugValue = this.slugValue;
|
||||
let slugChanged;
|
||||
|
||||
if (user.get('slug') !== slugValue) {
|
||||
if (user.slug !== slugValue) {
|
||||
slugChanged = true;
|
||||
user.set('slug', slugValue);
|
||||
user.slug = slugValue;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -286,7 +299,7 @@ export default Controller.extend({
|
||||
windowProxy.replaceState({path: newPath}, '', newPath);
|
||||
}
|
||||
|
||||
this.set('dirtyAttributes', false);
|
||||
this.dirtyAttributes = false;
|
||||
this.notifications.closeAlerts('user.update');
|
||||
|
||||
return user;
|
||||
@ -297,11 +310,37 @@ export default Controller.extend({
|
||||
this.notifications.showAPIError(error, {key: 'user.update'});
|
||||
}
|
||||
}
|
||||
}).group('saveHandlers'),
|
||||
}
|
||||
|
||||
saveViaKeyboard: action(function (event) {
|
||||
@task
|
||||
*saveNewPasswordTask() {
|
||||
yield this.user.saveNewPasswordTask.perform();
|
||||
document.querySelector('#password-reset')?.reset();
|
||||
}
|
||||
|
||||
@action
|
||||
submitPasswordForm(event) {
|
||||
event.preventDefault();
|
||||
this._blurAndTrigger(() => this.saveNewPasswordTask.perform());
|
||||
}
|
||||
|
||||
@action
|
||||
saveViaKeyboard(event) {
|
||||
event.preventDefault();
|
||||
this._blurAndTrigger(() => this.saveTask.perform());
|
||||
}
|
||||
|
||||
@task
|
||||
*copyContentKeyTask() {
|
||||
copyTextToClipboard(this.personalToken);
|
||||
yield timeout(this.isTesting ? 50 : 3000);
|
||||
}
|
||||
|
||||
clearScratchValues() {
|
||||
this.scratchValues = new TrackedObject();
|
||||
}
|
||||
|
||||
_blurAndTrigger(fn) {
|
||||
// trigger any set-on-blur actions
|
||||
const focusedElement = document.activeElement;
|
||||
focusedElement?.blur();
|
||||
@ -309,12 +348,7 @@ export default Controller.extend({
|
||||
// schedule save for when set-on-blur actions have finished
|
||||
run.schedule('actions', this, function () {
|
||||
focusedElement?.focus();
|
||||
this.save.perform();
|
||||
fn();
|
||||
});
|
||||
}),
|
||||
|
||||
copyContentKey: task(function* () {
|
||||
copyTextToClipboard(this.personalToken);
|
||||
yield timeout(this.isTesting ? 50 : 3000);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -97,8 +97,8 @@ export default BaseModel.extend(ValidationEngine, {
|
||||
return this.coverImage || defaultPath;
|
||||
}),
|
||||
|
||||
saveNewPassword: task(function* () {
|
||||
let validation = this.isLoggedIn ? 'ownPasswordChange' : 'passwordChange';
|
||||
saveNewPasswordTask: task(function* () {
|
||||
const validation = this.isLoggedIn ? 'ownPasswordChange' : 'passwordChange';
|
||||
|
||||
try {
|
||||
yield this.validate({property: validation});
|
||||
@ -108,7 +108,7 @@ export default BaseModel.extend(ValidationEngine, {
|
||||
}
|
||||
|
||||
try {
|
||||
let url = this.get('ghostPaths.url').api('users', 'password');
|
||||
let url = this.ghostPaths.url.api('users', 'password');
|
||||
|
||||
yield this.ajax.put(url, {
|
||||
data: {
|
||||
@ -121,16 +121,14 @@ export default BaseModel.extend(ValidationEngine, {
|
||||
}
|
||||
});
|
||||
|
||||
this.setProperties({
|
||||
password: '',
|
||||
newPassword: '',
|
||||
ne2Password: ''
|
||||
});
|
||||
this.password = '';
|
||||
this.newPassword = '';
|
||||
this.ne2Password = '';
|
||||
|
||||
this.notifications.showNotification('Password updated', {type: 'success', key: 'user.change-password.success'});
|
||||
|
||||
// clear errors manually for ne2password because validation
|
||||
// engine only clears the "validated proeprty"
|
||||
// engine only clears the "validated property"
|
||||
// TODO: clean up once we have a better validations library
|
||||
this.errors.remove('ne2Password');
|
||||
|
||||
|
17
ghost/admin/app/modifiers/select-on-click.js
Normal file
17
ghost/admin/app/modifiers/select-on-click.js
Normal file
@ -0,0 +1,17 @@
|
||||
import Modifier from 'ember-modifier';
|
||||
import {action} from '@ember/object';
|
||||
import {registerDestructor} from '@ember/destroyable';
|
||||
|
||||
export default class SelectOnClickModifier extends Modifier {
|
||||
modify(element) {
|
||||
element.addEventListener('click', this.onClick);
|
||||
registerDestructor(this, () => {
|
||||
element.removeEventListener('click', this.onClick);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onClick(event) {
|
||||
event.currentTarget.select();
|
||||
}
|
||||
}
|
@ -15,20 +15,20 @@ export default class UserRoute extends AuthenticatedRoute {
|
||||
|
||||
const currentUser = this.session.user;
|
||||
|
||||
let isOwnProfile = user.get('id') === currentUser.get('id');
|
||||
let isAuthorOrContributor = currentUser.get('isAuthorOrContributor');
|
||||
let isEditor = currentUser.get('isEditor');
|
||||
let isOwnProfile = user.id === currentUser.id;
|
||||
let isAuthorOrContributor = currentUser.isAuthorOrContributor;
|
||||
let isEditor = currentUser.isEditor;
|
||||
|
||||
if (isAuthorOrContributor && !isOwnProfile) {
|
||||
this.transitionTo('settings.staff.user', currentUser);
|
||||
} else if (isEditor && !isOwnProfile && !user.get('isAuthorOrContributor')) {
|
||||
} else if (isEditor && !isOwnProfile && !user.isAuthorOrContributor) {
|
||||
this.transitionTo('settings.staff');
|
||||
}
|
||||
|
||||
if (isOwnProfile) {
|
||||
this.store.queryRecord('api-key', {id: 'me'}).then((apiKey) => {
|
||||
this.controller.set('personalToken', apiKey.id + ':' + apiKey.secret);
|
||||
this.controller.set('personalTokenRegenerated', false);
|
||||
this.controller.personalToken = apiKey.id + ':' + apiKey.secret;
|
||||
this.controller.personalTokenRegenerated = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -37,6 +37,11 @@ export default class UserRoute extends AuthenticatedRoute {
|
||||
return {user_slug: model.get('slug')};
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.model = model;
|
||||
controller.reset();
|
||||
}
|
||||
|
||||
@action
|
||||
async willTransition(transition) {
|
||||
if (this.hasConfirmed) {
|
||||
|
@ -29,7 +29,7 @@
|
||||
<section class="view-actions">
|
||||
{{#if (or this.userActionsAreVisible this.session.user.isAdmin)}}
|
||||
<span class="dropdown">
|
||||
<GhDropdownButton @dropdownName="user-actions-menu" @classNames="gh-btn gh-btn-white gh-btn-icon icon-only user-actions-cog" @title="User Actions" data-test-user-actions={{true}}>
|
||||
<GhDropdownButton @dropdownName="user-actions-menu" @classNames="gh-btn gh-btn-white gh-btn-icon icon-only user-actions-cog" @title="User Actions" data-test-user-actions>
|
||||
<span>
|
||||
{{svg-jar "settings"}}
|
||||
<span class="hidden">User Settings</span>
|
||||
@ -74,7 +74,7 @@
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
<GhTaskButton @class="gh-btn gh-btn-primary gh-btn-icon" @task={{this.save}} data-test-save-button={{true}} />
|
||||
<GhTaskButton @class="gh-btn gh-btn-primary gh-btn-icon" @task={{this.saveTask}} data-test-save-button />
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
|
||||
@ -85,7 +85,7 @@
|
||||
{{!-- <div class="bg-"> --}}
|
||||
<section>
|
||||
<div class="gm-main view-container settings-user">
|
||||
<form id="user-settings-form" class="user-profile" novalidate="novalidate" autocomplete="off" {{on "submit" (perform this.save)}}>
|
||||
<form id="user-settings-form" class="user-profile" novalidate="novalidate" autocomplete="off" {{on "submit" (perform this.saveTask)}}>
|
||||
|
||||
<figure class="user-cover" style={{background-image-style this.user.coverImageUrl}}>
|
||||
<button type="button" class="gh-btn gh-btn-default user-cover-edit" {{on "click" this.changeCoverImage}}><span>Change cover</span></button>
|
||||
@ -101,14 +101,15 @@
|
||||
|
||||
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="name" @class="first-form-group">
|
||||
<label for="user-name">Full name</label>
|
||||
<GhTextInput
|
||||
@id="user-name"
|
||||
@class="user-name"
|
||||
@autocorrect="off"
|
||||
@value={{readonly this.user.name}}
|
||||
@input={{action (mut this.user.name) value="target.value"}}
|
||||
@focus-out={{action "validate" "name" target=this.user}}
|
||||
data-test-name-input={{true}}
|
||||
<input
|
||||
type="text"
|
||||
id="user-name"
|
||||
class="gh-input user-name"
|
||||
autocorrect="off"
|
||||
value={{this.user.name}}
|
||||
{{on "input" (fn this.setModelProperty "name")}}
|
||||
{{on "blur" (fn this.validateModelProperty "name")}}
|
||||
data-test-name-input
|
||||
/>
|
||||
{{#if this.user.errors.name}}
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="name" data-test-error="user-name" />
|
||||
@ -119,16 +120,16 @@
|
||||
|
||||
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="slug">
|
||||
<label for="user-slug">Slug</label>
|
||||
<GhTextInput
|
||||
@class="user-name"
|
||||
@id="user-slug"
|
||||
@name="user"
|
||||
@selectOnClick="true"
|
||||
@autocorrect="off"
|
||||
@value={{readonly this.slugValue}}
|
||||
@input={{action (mut this.slugValue) value="target.value"}}
|
||||
@focus-out={{action (perform this.updateSlug this.slugValue)}}
|
||||
data-test-slug-input={{true}}
|
||||
<input
|
||||
type="text"
|
||||
id="user-slug"
|
||||
class="gh-input user-slug"
|
||||
autocorrect="off"
|
||||
value={{this.slugValue}}
|
||||
{{on "input" this.setSlugValue}}
|
||||
{{on "blur" (perform this.updateSlugTask)}}
|
||||
{{select-on-click}}
|
||||
data-test-slug-input
|
||||
/>
|
||||
<p><GhBlogUrl />/author/{{this.slugValue}}</p>
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="slug" data-test-error="user-slug" />
|
||||
@ -138,18 +139,18 @@
|
||||
<label for="user-email">Email</label>
|
||||
{{!-- Administrators only see text of Owner's email address but not input --}}
|
||||
{{#if this.canChangeEmail}}
|
||||
<GhTextInput
|
||||
@type="email"
|
||||
@id="user-email"
|
||||
@name="email"
|
||||
@placeholder="jamie@example.com"
|
||||
@autocapitalize="off"
|
||||
@autocorrect="off"
|
||||
@autocomplete="off"
|
||||
@value={{readonly this.user.email}}
|
||||
@input={{action (mut this.user.email) value="target.value"}}
|
||||
@focus-out={{action "validate" "email" target=this.user}}
|
||||
data-test-email-input={{true}}
|
||||
<input
|
||||
type="email"
|
||||
id="user-email"
|
||||
class="gh-input"
|
||||
placeholder="jamie@example.com"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
value={{this.user.email}}
|
||||
{{on "input" (fn this.setModelProperty "email")}}
|
||||
{{on "blur" (fn this.validateModelProperty "email")}}
|
||||
data-test-email-input
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="email" data-test-error="user-email" />
|
||||
{{else}}
|
||||
@ -168,28 +169,33 @@
|
||||
|
||||
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="location">
|
||||
<label for="user-location">Location</label>
|
||||
<GhTextInput
|
||||
@id="user-location"
|
||||
@value={{readonly this.user.location}}
|
||||
@input={{action (mut this.user.location) value="target.value"}}
|
||||
@focus-out={{action "validate" "location" target=this.user}}
|
||||
data-test-location-input={{true}} />
|
||||
<input
|
||||
type="text"
|
||||
id="user-location"
|
||||
class="gh-input"
|
||||
value={{this.user.location}}
|
||||
{{on "input" (fn this.setModelProperty "location")}}
|
||||
{{on "blur" (fn this.validateModelProperty "location")}}
|
||||
data-test-location-input
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="location" data-test-error="user-location" />
|
||||
<p>Where in the world do you live?</p>
|
||||
</GhFormGroup>
|
||||
|
||||
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="website">
|
||||
<label for="user-website">Website</label>
|
||||
<GhTextInput
|
||||
@type="url"
|
||||
@id="user-website"
|
||||
@autocapitalize="off"
|
||||
@autocorrect="off"
|
||||
@autocomplete="off"
|
||||
@value={{readonly this.user.website}}
|
||||
@input={{action (mut this.user.website) value="target.value"}}
|
||||
@focus-out={{action "validate" "website" target=this.user}}
|
||||
data-test-website-input={{true}} />
|
||||
<input
|
||||
type="url"
|
||||
id="user-website"
|
||||
class="gh-input"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
value={{this.user.website}}
|
||||
{{on "input" (fn this.setModelProperty "website")}}
|
||||
{{on "blur" (fn this.validateModelProperty "website")}}
|
||||
data-test-website-input
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="website" data-test-error="user-website" />
|
||||
<p>Have a website or blog other than this one? Link it!</p>
|
||||
</GhFormGroup>
|
||||
@ -222,13 +228,13 @@
|
||||
|
||||
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="bio" @class="bio-container">
|
||||
<label for="user-bio">Bio</label>
|
||||
<GhTextarea
|
||||
@id="user-bio"
|
||||
@value={{readonly this.user.bio}}
|
||||
@input={{action (mut this.user.bio) value="target.value"}}
|
||||
@focus-out={{action "validate" "bio" target=this.user}}
|
||||
data-test-bio-input={{true}}
|
||||
/>
|
||||
<textarea
|
||||
id="user-bio"
|
||||
class="gh-input"
|
||||
{{on "input" (fn this.setModelProperty "bio")}}
|
||||
{{on "blur" (fn this.validateModelProperty "bio")}}
|
||||
data-test-bio-input
|
||||
>{{this.user.bio}}</textarea>
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="bio" data-test-error="user-bio" />
|
||||
<p>
|
||||
Recommended: <strong>200</strong> characters.
|
||||
@ -331,22 +337,21 @@
|
||||
|
||||
{{!-- If an administrator is viewing Owner's profile then hide inputs for change password --}}
|
||||
{{#if this.canChangePassword}}
|
||||
<form id="password-reset" class="user-profile" novalidate="novalidate" autocomplete="off" {{on "submit" (perform this.user.saveNewPassword)}}>
|
||||
<form id="password-reset" class="user-profile" novalidate="novalidate" autocomplete="off" {{on "submit" this.submitPasswordForm}}>
|
||||
<div class="pa5">
|
||||
<fieldset class="user-details-form">
|
||||
{{#if this.isOwnProfile}}
|
||||
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="password">
|
||||
<label for="user-password-old">Old password</label>
|
||||
<GhTextInput
|
||||
@type="password"
|
||||
@id="user-password-old"
|
||||
@autocomplete="current-password"
|
||||
@value={{readonly this.user.password}}
|
||||
@input={{action "updatePassword" value="target.value"}}
|
||||
@keyEvents={{hash
|
||||
Enter=(action (perform this.user.saveNewPassword))
|
||||
}}
|
||||
data-test-old-pass-input={{true}}
|
||||
<input
|
||||
type="password"
|
||||
id="user-password-old"
|
||||
class="gh-input"
|
||||
autocomplete="current-password"
|
||||
value={{this.user.password}}
|
||||
{{on "input" (fn this.setModelProperty "password")}}
|
||||
{{on "input" (fn this.clearModelErrors "password")}}
|
||||
data-test-old-pass-input
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="password" data-test-error="user-old-pass" />
|
||||
</GhFormGroup>
|
||||
@ -354,37 +359,35 @@
|
||||
|
||||
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="newPassword">
|
||||
<label for="user-password-new">New password</label>
|
||||
<GhTextInput
|
||||
@value={{readonly this.user.newPassword}}
|
||||
@type="password"
|
||||
@autocomplete="new-password"
|
||||
@id="user-password-new"
|
||||
@input={{action "updateNewPassword" value="target.value"}}
|
||||
@keyEvents={{hash
|
||||
Enter=(action (perform this.user.saveNewPassword))
|
||||
}}
|
||||
data-test-new-pass-input={{true}}
|
||||
<input
|
||||
type="password"
|
||||
id="user-password-new"
|
||||
class="gh-input"
|
||||
autocomplete="new-password"
|
||||
value={{this.user.newPassword}}
|
||||
{{on "input" (fn this.setModelProperty "newPassword")}}
|
||||
{{on "input" (fn this.clearModelErrors "newPassword")}}
|
||||
data-test-new-pass-input
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="newPassword" data-test-error="user-new-pass" />
|
||||
</GhFormGroup>
|
||||
|
||||
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="ne2Password">
|
||||
<label for="user-new-password-verification">Verify password</label>
|
||||
<GhTextInput
|
||||
@value={{readonly this.user.ne2Password}}
|
||||
@type="password"
|
||||
@id="user-new-password-verification"
|
||||
@input={{action "updateNe2Password" value="target.value"}}
|
||||
@keyEvents={{hash
|
||||
Enter=(action (perform this.user.saveNewPassword))
|
||||
}}
|
||||
data-test-ne2-pass-input={{true}}
|
||||
<input
|
||||
type="password"
|
||||
id="user-new-password-verification"
|
||||
class="gh-input"
|
||||
value={{this.user.ne2Password}}
|
||||
{{on "input" (fn this.setModelProperty "ne2Password")}}
|
||||
{{on "input" (fn this.clearModelErrors "ne2Password")}}
|
||||
data-test-ne2-pass-input
|
||||
/>
|
||||
<GhErrorMessage @errors={{this.user.errors}} @property="ne2Password" data-test-error="user-ne2-pass" />
|
||||
</GhFormGroup>
|
||||
|
||||
<div class="form-group">
|
||||
<GhTaskButton @buttonText="Change Password" @idleClass="gh-btn-red" @class="gh-btn gh-btn-icon button-change-password" @task={{this.user.saveNewPassword}} data-test-save-pw-button="true" />
|
||||
<GhTaskButton @buttonText="Change Password" @idleClass="gh-btn-red" @class="gh-btn gh-btn-icon button-change-password" @task={{this.saveNewPasswordTask}} data-test-save-pw-button="true" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@ -397,20 +400,21 @@
|
||||
<fieldset class="user-details-form">
|
||||
<GhFormGroup>
|
||||
<label for="personal-token">Staff access token</label>
|
||||
<div class="relative flex items-center {{unless this.copyContentKey.isRunning "hide-child-instant"}}">
|
||||
<GhTextInput
|
||||
@id="personal-token"
|
||||
@value={{readonly this.personalToken}}
|
||||
@readonly
|
||||
@type="text"
|
||||
onclick="this.select()"
|
||||
/>
|
||||
<div class="relative flex items-center {{unless this.copyContentKeyTask.isRunning "hide-child-instant"}}">
|
||||
<input
|
||||
type="text"
|
||||
id="personal-token"
|
||||
class="gh-input"
|
||||
value={{this.personalToken}}
|
||||
readonly
|
||||
{{select-on-click}}
|
||||
/>
|
||||
<div class="app-api-personal-token-buttons child">
|
||||
<button type="button" class="app-button-regenerate" {{on "click" this.regenerateStaffToken}} data-tooltip="Regenerate">
|
||||
{{svg-jar "reload" class="w4 h4 stroke-midgrey"}}
|
||||
</button>
|
||||
<button type="button" {{action (perform this.copyContentKey)}} class="app-button-copy">
|
||||
{{#if this.copyContentKey.isRunning}}
|
||||
<button type="button" class="app-button-copy" {{on "click" (perform this.copyContentKeyTask)}}>
|
||||
{{#if this.copyContentKeyTask.isRunning}}
|
||||
{{svg-jar "check-circle" class="w3 v-mid mr2 stroke-white"}} Copied
|
||||
{{else}}
|
||||
Copy
|
||||
|
@ -12,8 +12,7 @@ import {
|
||||
find,
|
||||
findAll,
|
||||
focus,
|
||||
triggerEvent,
|
||||
triggerKeyEvent
|
||||
triggerEvent
|
||||
} from '@ember/test-helpers';
|
||||
import {enableLabsFlag} from '../helpers/labs-flag';
|
||||
import {enableMembers} from '../helpers/members';
|
||||
@ -22,6 +21,7 @@ import {expect} from 'chai';
|
||||
import {keyDown} from 'ember-keyboard/test-support/test-helpers';
|
||||
import {setupApplicationTest} from 'ember-mocha';
|
||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||
import {submitForm} from '../helpers/forms';
|
||||
import {visit} from '../helpers/visit';
|
||||
|
||||
describe('Acceptance: Staff', function () {
|
||||
@ -456,7 +456,8 @@ describe('Acceptance: Staff', function () {
|
||||
});
|
||||
|
||||
describe('existing user', function () {
|
||||
let user, newLocation, originalReplaceState;
|
||||
let user, originalReplaceState;
|
||||
let newLocation; // eslint-disable-line
|
||||
|
||||
beforeEach(function () {
|
||||
user = this.server.create('user', {
|
||||
@ -511,7 +512,7 @@ describe('Acceptance: Staff', function () {
|
||||
// Save changes
|
||||
await click('[data-test-save-button]');
|
||||
|
||||
// Since we reset save status so there's no on-screen indication
|
||||
// Since we reset save button's status there's no on-screen indication
|
||||
// that we've had a save, check the request was fired instead
|
||||
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
|
||||
let params = JSON.parse(lastRequest.requestBody);
|
||||
@ -519,19 +520,16 @@ describe('Acceptance: Staff', function () {
|
||||
expect(params.users[0].name).to.equal('Test User');
|
||||
|
||||
// CMD-S shortcut works
|
||||
await fillIn('[data-test-slug-input]', 'New Slug');
|
||||
await fillIn('[data-test-name-input]', 'New Name');
|
||||
await keyDown('cmd+s');
|
||||
|
||||
// Since we reset save status so there's no on-screen indication
|
||||
// Since we reset save button's status there's no on-screen indication
|
||||
// that we've had a save, check the request was fired instead
|
||||
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
|
||||
params = JSON.parse(lastRequest.requestBody);
|
||||
|
||||
// slug should have been correctly slugified before save
|
||||
expect(params.users[0].slug).to.equal('new-slug');
|
||||
|
||||
// check that the history state has been updated
|
||||
expect(newLocation).to.equal('new-slug');
|
||||
// name should have been correctly set before save (we set on blur)
|
||||
expect(params.users[0].name, 'saved name').to.equal('New Name');
|
||||
|
||||
await fillIn('[data-test-slug-input]', 'white space');
|
||||
await blur('[data-test-slug-input]');
|
||||
@ -703,7 +701,7 @@ describe('Acceptance: Staff', function () {
|
||||
await fillIn('[data-test-ne2-pass-input]', 'notlong');
|
||||
|
||||
// enter key triggers action
|
||||
await triggerKeyEvent('[data-test-new-pass-input]', 'keyup', 13);
|
||||
await submitForm('[data-test-new-pass-input]');
|
||||
|
||||
expect(
|
||||
find('[data-test-new-pass-input]').closest('.form-group'),
|
||||
@ -720,7 +718,7 @@ describe('Acceptance: Staff', function () {
|
||||
await fillIn('[data-test-ne2-pass-input]', 'ghostisawesome');
|
||||
|
||||
// enter key triggers action
|
||||
await triggerKeyEvent('#user-password-new', 'keyup', 13);
|
||||
await submitForm('#user-password-new');
|
||||
|
||||
expect(
|
||||
find('#user-password-new').closest('.form-group'),
|
||||
@ -742,7 +740,7 @@ describe('Acceptance: Staff', function () {
|
||||
).to.not.have.class('error');
|
||||
|
||||
// enter key triggers action
|
||||
await triggerKeyEvent('[data-test-new-pass-input]', 'keyup', 13);
|
||||
await submitForm('[data-test-new-pass-input]');
|
||||
|
||||
expect(
|
||||
find('[data-test-ne2-pass-input]').closest('.form-group'),
|
||||
|
10
ghost/admin/tests/helpers/forms.js
Normal file
10
ghost/admin/tests/helpers/forms.js
Normal file
@ -0,0 +1,10 @@
|
||||
import {find, triggerEvent} from '@ember/test-helpers';
|
||||
|
||||
export async function submitForm(elementOrSelector) {
|
||||
if (typeof elementOrSelector === 'string') {
|
||||
elementOrSelector = find(elementOrSelector);
|
||||
}
|
||||
|
||||
const form = elementOrSelector.closest('form');
|
||||
await triggerEvent(form, 'submit');
|
||||
}
|
Loading…
Reference in New Issue
Block a user