diff --git a/ghost/admin/app/controllers/member.js b/ghost/admin/app/controllers/member.js index 96857501b27..e0d9f1bf864 100644 --- a/ghost/admin/app/controllers/member.js +++ b/ghost/admin/app/controllers/member.js @@ -24,6 +24,9 @@ export default class MemberController extends Controller { @tracked modalLabel = null; @tracked showLabelModal = false; + _previousLabels = null; + _previousNewsletters = null; + constructor() { super(...arguments); this._availableLabels = this.store.peekAll('label'); @@ -39,6 +42,18 @@ export default class MemberController extends Controller { 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; @@ -76,6 +91,12 @@ export default class MemberController extends Controller { // Actions ----------------------------------------------------------------- + @action + setInitialRelationshipValues() { + this._previousLabels = this._labels; + this._previousNewsletters = this._newsletters; + } + @action toggleLabelModal() { this.showLabelModal = !this.showLabelModal; @@ -140,6 +161,8 @@ export default class MemberController extends Controller { member.updateLabels(); this.members.refreshData(); + this.setInitialRelationshipValues(); + // replace 'member.new' route with 'member' route this.replaceRoute('member', member); @@ -172,6 +195,8 @@ export default class MemberController extends Controller { include: 'tiers' }); + this.setInitialRelationshipValues(); + this.isLoading = false; } @@ -191,4 +216,36 @@ export default class MemberController extends Controller { this.member.set(propKey, newValue); } + + _hasDirtyAttributes() { + let member = this.member; + + if (!member) { + 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; + } } diff --git a/ghost/admin/app/routes/member.js b/ghost/admin/app/routes/member.js index 905b70be4e1..6b77844a707 100644 --- a/ghost/admin/app/routes/member.js +++ b/ghost/admin/app/routes/member.js @@ -50,42 +50,39 @@ export default class MembersRoute extends AdminRoute { @action async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); + let hasDirtyAttributes = this.controller.dirtyAttributes; // wait for any existing confirm modal to be closed before allowing transition if (this.confirmModal) { return; } - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } + if (!this.hasConfirmed && hasDirtyAttributes) { + transition.abort(); - const shouldLeave = await this.confirmUnsavedChanges(); + if (this.controller.saveTask?.isRunning) { + await this.controller.saveTask.last; + transition.retry(); + } - if (shouldLeave) { - this.controller.model.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); + const shouldLeave = await this.confirmUnsavedChanges(); + + if (shouldLeave) { + this.controller.model.rollbackAttributes(); + this.hasConfirmed = true; + return transition.retry(); + } } } async confirmUnsavedChanges() { - if (this.controller.model?.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } + this.confirmModal = this.modals + .open(ConfirmUnsavedChangesModal) + .finally(() => { + this.confirmModal = null; + }); - return true; + return this.confirmModal; } closeImpersonateModal(transition) {