Ghost/ghost/admin/app/controllers/member.js
Simon Backx 17a6217cc7 🐛 Fixed members breadcrumbs when not coming from analytics
fixes https://github.com/TryGhost/Team/issues/2404

This change introduces a new 'post' query parameter to the members and member routes.

Previously, the members route would check if the previous route was the analytics page, and then show the breadcrumbs to go back to the analytics page. But when navigating to the members page from the menu, we don't want to show the breadcrumbs. To accomplish this, the routes that point to the members page from the analytics page now specifically pass on the post id in the query parameters. The query parameter is then passed on from the members page to the member page.

`directlyFromAnalytics` is still used in the member route, to know wheter we came from the members page or from the analytics page (changes the breadcrumbs). This doesn't need to go via a query parameter (figured that would make the url too long/complex).

The resetController method is now implemented and resets the filter and/or fromAnalytics post id if required (when going from members to member, we don't want to reset it because the we would lose the filter going back).
2023-05-04 11:20:33 +02:00

265 lines
7.6 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;
queryParams = [
{postAnalytics: 'post'}
];
@tracked isLoading = false;
@tracked showImpersonateMemberModal = false;
@tracked modalLabel = null;
@tracked showLabelModal = false;
_previousLabels = null;
_previousNewsletters = null;
@tracked directlyFromAnalytics = false;
@tracked postAnalytics = null;
get fromAnalytics() {
if (!this.postAnalytics) {
return null;
}
return [this.postAnalytics];
}
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;
}
}