From 48b033f1e1ba06414d08eaa3d1dc26c200bf77e8 Mon Sep 17 00:00:00 2001 From: Hakim Razalan Date: Sat, 22 Oct 2022 04:05:14 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20missing=20unsaved=20chan?= =?UTF-8?q?ges=20modal=20for=20member=20newsletters=20(#15564)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes: https://github.com/TryGhost/Ghost/issues/15507 - manually handle relationship changes detection labels and newsletters - add `dirtyAttributes` controller property - return newsletters and labels dirty attributes status --- ghost/admin/app/controllers/member.js | 57 +++++++++++++++++++++++++++ ghost/admin/app/routes/member.js | 43 ++++++++++---------- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/ghost/admin/app/controllers/member.js b/ghost/admin/app/controllers/member.js index 563b9f11be..2ed9233a44 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; @@ -75,6 +90,12 @@ export default class MemberController extends Controller { // Actions ----------------------------------------------------------------- + @action + setInitialRelationshipValues() { + this._previousLabels = this._labels; + this._previousNewsletters = this._newsletters; + } + @action toggleLabelModal() { this.showLabelModal = !this.showLabelModal; @@ -139,6 +160,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); @@ -171,6 +194,8 @@ export default class MemberController extends Controller { include: 'tiers' }); + this.setInitialRelationshipValues(); + this.isLoading = false; } @@ -190,4 +215,36 @@ export default class MemberController extends Controller { this.member[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 b1795592a2..4a178d1503 100644 --- a/ghost/admin/app/routes/member.js +++ b/ghost/admin/app/routes/member.js @@ -48,42 +48,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; - }); + this.confirmModal = this.modals + .open(ConfirmUnsavedChangesModal) + .finally(() => { + this.confirmModal = null; + }); - return this.confirmModal; - } - - return true; + return this.confirmModal; } closeImpersonateModal(transition) {