mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-29 22:01:49 +03:00
3817f583fa
closes https://github.com/TryGhost/Team/issues/2275 When deleting a member, after confirming deletion another "unsaved changes" modal popped up. From that point, if you clicked to stay you remained on the member screen with stale data (the member was still deleted) resulting in further errors when any attempt to make changes was made. - prevented the unsaved changes check running for a deleted member because it would always return `true` in that case - ensured the data setup for the unsaved changes check still occurs when a member is accessed directly via the URL - previously it was skipped because the data setup only occurred inside `fetchMemberTask` but that isn't called when the route already loaded the model via it's `model()` hook
254 lines
7.4 KiB
JavaScript
254 lines
7.4 KiB
JavaScript
import Controller, {inject as controller} from '@ember/controller';
|
|
import DeleteMemberModal from '../components/members/modals/delete-member';
|
|
import EmberObject, {action, defineProperty} from '@ember/object';
|
|
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
|
import moment from 'moment-timezone';
|
|
import {inject as service} from '@ember/service';
|
|
import {task} from 'ember-concurrency';
|
|
import {tracked} from '@glimmer/tracking';
|
|
|
|
const SCRATCH_PROPS = ['name', 'email', 'note'];
|
|
|
|
export default class MemberController extends Controller {
|
|
@controller members;
|
|
@service session;
|
|
@service dropdown;
|
|
@service membersStats;
|
|
@service modals;
|
|
@service notifications;
|
|
@service router;
|
|
@service store;
|
|
|
|
@tracked isLoading = false;
|
|
@tracked showImpersonateMemberModal = false;
|
|
@tracked modalLabel = null;
|
|
@tracked showLabelModal = false;
|
|
|
|
_previousLabels = null;
|
|
_previousNewsletters = null;
|
|
|
|
directlyFromAnalytics = false;
|
|
fromAnalytics = null;
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
this._availableLabels = this.store.peekAll('label');
|
|
}
|
|
|
|
// Computed properties -----------------------------------------------------
|
|
|
|
get member() {
|
|
return this.model;
|
|
}
|
|
|
|
set member(member) {
|
|
this.model = member;
|
|
}
|
|
|
|
get dirtyAttributes() {
|
|
return this._hasDirtyAttributes();
|
|
}
|
|
|
|
get _labels() {
|
|
return this.member.get('labels').map(label => label.name);
|
|
}
|
|
|
|
get _newsletters() {
|
|
return this.member.get('newsletters').map(newsletter => newsletter.id);
|
|
}
|
|
|
|
get labelModalData() {
|
|
let label = this.modalLabel;
|
|
let labels = this.availableLabels;
|
|
|
|
return {
|
|
label,
|
|
labels
|
|
};
|
|
}
|
|
|
|
get availableLabels() {
|
|
let labels = this._availableLabels
|
|
.filter(label => !label.isNew)
|
|
.filter(label => label.id !== null)
|
|
.sort((labelA, labelB) => labelA.name.localeCompare(labelB.name, undefined, {ignorePunctuation: true}));
|
|
let options = labels.toArray();
|
|
|
|
options.unshiftObject({name: 'All labels', slug: null});
|
|
|
|
return options;
|
|
}
|
|
|
|
get scratchMember() {
|
|
let scratchMember = EmberObject.create({member: this.member});
|
|
SCRATCH_PROPS.forEach(prop => defineProperty(scratchMember, prop, boundOneWay(`member.${prop}`)));
|
|
return scratchMember;
|
|
}
|
|
|
|
get subscribedAt() {
|
|
let memberSince = moment(this.member.createdAtUTC).from(moment());
|
|
let createdDate = moment(this.member.createdAtUTC).format('D MMM YYYY');
|
|
return `${createdDate} (${memberSince})`;
|
|
}
|
|
|
|
// Actions -----------------------------------------------------------------
|
|
|
|
@action
|
|
setInitialRelationshipValues() {
|
|
this._previousLabels = this._labels;
|
|
this._previousNewsletters = this._newsletters;
|
|
}
|
|
|
|
@action
|
|
toggleLabelModal() {
|
|
this.showLabelModal = !this.showLabelModal;
|
|
}
|
|
|
|
@action
|
|
editLabel(label, e) {
|
|
if (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
let modalLabel = this.availableLabels.findBy('slug', label);
|
|
this.modalLabel = modalLabel;
|
|
this.showLabelModal = !this.showLabelModal;
|
|
}
|
|
|
|
@action
|
|
setProperty(propKey, value) {
|
|
this._saveMemberProperty(propKey, value);
|
|
}
|
|
|
|
@action
|
|
confirmDeleteMember() {
|
|
this.modals.open(DeleteMemberModal, {
|
|
member: this.member,
|
|
afterDelete: () => {
|
|
this.membersStats.invalidate();
|
|
this.members.refreshData();
|
|
this.transitionToRoute('members');
|
|
}
|
|
});
|
|
}
|
|
|
|
@action
|
|
toggleImpersonateMemberModal() {
|
|
this.showImpersonateMemberModal = !this.showImpersonateMemberModal;
|
|
}
|
|
|
|
@action
|
|
closeImpersonateMemberModal() {
|
|
this.showImpersonateMemberModal = false;
|
|
}
|
|
|
|
@action
|
|
save() {
|
|
return this.saveTask.perform();
|
|
}
|
|
|
|
// Tasks -------------------------------------------------------------------
|
|
|
|
@task({drop: true})
|
|
*saveTask() {
|
|
let {member, scratchMember} = this;
|
|
|
|
// if Cmd+S is pressed before the field loses focus make sure we're
|
|
// saving the intended property values
|
|
let scratchProps = scratchMember.getProperties(SCRATCH_PROPS);
|
|
Object.assign(member, scratchProps);
|
|
|
|
try {
|
|
yield member.save();
|
|
member.updateLabels();
|
|
this.members.refreshData();
|
|
|
|
this.setInitialRelationshipValues();
|
|
|
|
// replace 'member.new' route with 'member' route
|
|
this.replaceRoute('member', member);
|
|
|
|
return member;
|
|
} catch (error) {
|
|
if (error === undefined) {
|
|
// Validation error
|
|
return;
|
|
}
|
|
|
|
if (error.payload && error.payload.errors) {
|
|
for (const payloadError of error.payload.errors) {
|
|
if (payloadError.type === 'ValidationError' && payloadError.property && (payloadError.context || payloadError.message)) {
|
|
member.errors.add(payloadError.property, payloadError.context || payloadError.message);
|
|
member.hasValidated.pushObject(payloadError.property);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
@task
|
|
*fetchMemberTask(memberId) {
|
|
this.isLoading = true;
|
|
|
|
this.member = yield this.store.queryRecord('member', {
|
|
id: memberId,
|
|
include: 'tiers'
|
|
});
|
|
|
|
this.setInitialRelationshipValues();
|
|
|
|
this.isLoading = false;
|
|
}
|
|
|
|
// Private -----------------------------------------------------------------
|
|
|
|
_saveMemberProperty(propKey, newValue) {
|
|
let currentValue = this.member[propKey];
|
|
|
|
if (newValue && typeof newValue === 'string') {
|
|
newValue = newValue.trim();
|
|
}
|
|
|
|
// avoid modifying empty values and triggering inadvertant unsaved changes modals
|
|
if (newValue !== false && !newValue && !currentValue) {
|
|
return;
|
|
}
|
|
|
|
this.member[propKey] = newValue;
|
|
}
|
|
|
|
_hasDirtyAttributes() {
|
|
let member = this.member;
|
|
|
|
if (!member || member.isDeleted || member.isDeleting) {
|
|
return false;
|
|
}
|
|
|
|
// member.labels is an array so hasDirtyAttributes doesn't pick up
|
|
// changes unless the array ref is changed.
|
|
// use sort() to sort of detect same item is re-added
|
|
let currentLabels = (this._labels.sort() || []).join(', ');
|
|
let previousLabels = (this._previousLabels.sort() || []).join(', ');
|
|
if (currentLabels !== previousLabels) {
|
|
return true;
|
|
}
|
|
|
|
// member.newsletters is an array so hasDirtyAttributes doesn't pick up
|
|
// changes unless the array ref is changed
|
|
// use sort() to sort of detect same item is re-enabled
|
|
let currentNewsletters = (this._newsletters.sort() || []).join(', ');
|
|
let previousNewsletters = (this._previousNewsletters.sort() || []).join(', ');
|
|
if (currentNewsletters !== previousNewsletters) {
|
|
return true;
|
|
}
|
|
|
|
// we've covered all the non-tracked cases we care about so fall
|
|
// back on Ember Data's default dirty attribute checks
|
|
let {hasDirtyAttributes} = member;
|
|
|
|
return hasDirtyAttributes;
|
|
}
|
|
}
|