Refactored modal for bulk removing a label to members

refs https://github.com/TryGhost/Team/issues/559

Members controller was becoming bloated and difficult to follow due to catering for many different concerns.

- converted old modal to newer promise-modal style
- pulled full label-removing logic out of the members controller and into the modal so logic is contained in one place
This commit is contained in:
Kevin Ansfield 2022-02-09 17:22:25 +00:00
parent 925751c678
commit efb7bc4c3b
6 changed files with 154 additions and 148 deletions

View File

@ -1,65 +0,0 @@
<header class="modal-header" data-test-modal="remove-label-members">
<h1>Remove Label</h1>
</header>
<a class="close" href="" role="button" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
{{#if this.confirmed}}
<div class="gh-content-box pa" data-test-state="remove-complete">
{{#if this.error}}
<div class="flex items-center">
{{svg-jar "warning" class="w4 h4 fill-red mr2 nudge-top--3"}}
<div>
<p class="ma0 pa0">
<span class="fw5" data-test-text="remove-error">{{this.error}}</span>
</p>
</div>
</div>
{{else}}
<div class="flex items-center">
{{svg-jar "check-circle" class="w4 h4 stroke-green mr2"}}
<p class="ma0 pa0">
Label removed from <span class="fw6" data-test-text="remove-count">{{gh-pluralize this.response.stats.successful "member"}}</span>
successfully
</p>
</div>
{{#if this.response.stats.unsuccessful}}
<div class="flex items-start mt2" data-test-bulk-label-add-errors>
{{svg-jar "warning" class="w4 h4 fill-red mr2 nudge-top--3"}}
<div>
<p class="ma0 pa0">
Failed to remove label from <span class="fw5" data-test-text="invalid-count">{{gh-pluralize this.response.stats.unsuccessful "member"}}</span>
</p>
</div>
</div>
{{/if}}
{{/if}}
</div>
{{else}}
<div class="modal-body" data-test-state="add-label-unconfirmed">
<GhMemberSingleLabelInput @onChange={{action "setLabel"}} @triggerId="label-input" data-test-input="" />
<p class="mt2 ml1">
Will be removed from the currently selected <span class="fw6" data-test-text="member-count">{{gh-pluralize this.model.memberCount "member"}}</span>
</p>
</div>
{{/if}}
<div class="modal-footer">
{{#if this.confirmed}}
<button class="gh-btn gh-btn-black" data-test-button="close-modal" type="button" {{action "closeModal"}}>
<span>Close</span>
</button>
{{else}}
<button class="gh-btn" data-test-button="cancel" type="button" {{action "closeModal"}}>
<span>Cancel</span>
</button>
<GhTaskButton
@disabled={{this.isDisabled}}
@buttonText="Remove Label"
@successText="Removed"
@task={{this.removeLabelTask}}
@class="gh-btn gh-btn-red gh-btn-icon"
data-test-button="confirm"
/>
{{/if}}
</div>

View File

@ -1,38 +0,0 @@
import ModalComponent from 'ghost-admin/components/modal-base';
import {alias, not} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default ModalComponent.extend({
membersStats: service(),
selectedLabel: null,
// Allowed actions
confirm: () => {},
isDisabled: not('selectedLabel'),
member: alias('model'),
actions: {
confirm() {
this.removeLabelTask.perform();
},
setLabel(label) {
this.set('selectedLabel', label);
}
},
removeLabelTask: task(function* () {
try {
const response = yield this.confirm(this.selectedLabel);
this.set('response', response);
this.set('confirmed', true);
} catch (e) {
if (e.payload?.errors) {
this.set('confirmed', true);
this.set('error', e.payload.errors[0].message);
}
throw e;
}
}).drop()
});

View File

@ -0,0 +1,81 @@
<div class="modal-content">
<header class="modal-header" data-test-modal="remove-label-members">
<h1>Remove Label</h1>
</header>
<a class="close" href="" role="button" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
{{#if this.hasRun}}
<div class="gh-content-box pa" data-test-state="remove-complete">
{{#if this.error}}
<div class="flex items-center">
{{svg-jar "warning" class="w4 h4 fill-red mr2 nudge-top--3"}}
<div>
<p class="ma0 pa0">
<span class="fw5" data-test-text="remove-error">{{this.error}}</span>
</p>
</div>
</div>
{{else}}
<div class="flex items-center">
{{svg-jar "check-circle" class="w4 h4 stroke-green mr2"}}
<p class="ma0 pa0">
Label removed from
<span class="fw6" data-test-text="remove-count">{{gh-pluralize this.response.stats.successful "member"}}</span>
successfully
</p>
</div>
{{#if this.response.stats.unsuccessful}}
<div class="flex items-start mt2" data-test-bulk-label-add-errors>
{{svg-jar "warning" class="w4 h4 fill-red mr2 nudge-top--3"}}
<div>
<p class="ma0 pa0">
Failed to remove label from <span class="fw5" data-test-text="invalid-count">{{gh-pluralize this.response.stats.unsuccessful "member"}}</span>
</p>
</div>
</div>
{{/if}}
{{/if}}
</div>
{{else}}
<div class="modal-body" data-test-state="remove-label-unconfirmed">
{{#if @data.query}}
{{#let (members-count-fetcher query=@data.query) as |countFetcher|}}
{{#if countFetcher.isLoading}}
<GhLoadingSpinner />
{{else}}
<GhMemberSingleLabelInput
@onChange={{this.setLabel}}
@triggerId="label-input"
/>
<p class="mt2 ml1">
Will be removed from the currently selected <span class="fw6" data-test-text="member-count">{{gh-pluralize countFetcher.count "member"}}</span>
</p>
{{/if}}
{{/let}}
{{else}}
<p>No members are selected.</p>
{{/if}}
</div>
{{/if}}
<div class="modal-footer">
{{#if this.hasRun}}
<button class="gh-btn gh-btn-black" data-test-button="close-modal" type="button" {{on "click" @close}}>
<span>Close</span>
</button>
{{else}}
<button class="gh-btn" data-test-button="cancel" type="button" {{on "click" @close}}>
<span>Cancel</span>
</button>
<GhTaskButton
@disabled={{this.isDisabled}}
@buttonText="Remove Label"
@successText="Removed"
@task={{this.removeLabelTask}}
@class="gh-btn gh-btn-red gh-btn-icon"
data-test-button="confirm"
/>
{{/if}}
</div>
</div>

View File

@ -0,0 +1,60 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class MembersBulkRemoveLabelModal extends Component {
@service ajax;
@service ghostPaths;
@tracked error;
@tracked response;
@tracked selectedLabel;
get isDisabled() {
return !this.args.data.query || !this.selectedLabel;
}
get hasRun() {
return !!(this.error || this.response);
}
@action
setLabel(label) {
this.selectedLabel = label;
}
@task({drop: true})
*removeLabelTask() {
try {
const query = new URLSearchParams(this.args.data.query);
const removeLabelUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
const response = yield this.ajax.put(removeLabelUrl, {
data: {
bulk: {
action: 'removeLabel',
meta: {
label: {
id: this.selectedLabel
}
}
}
}
});
this.args.data.onComplete?.();
this.response = response?.bulk?.meta;
return true;
} catch (e) {
if (e.payload?.errors) {
this.error = e.payload.errors[0].message;
} else {
this.error = 'An unknown error occurred. Please try again.';
}
throw e;
}
}
}

View File

@ -56,7 +56,6 @@ export default class MembersController extends Controller {
@tracked showLabelModal = false;
@tracked showDeleteMembersModal = false;
@tracked showUnsubscribeMembersModal = false;
@tracked showRemoveMembersLabelModal = false;
@tracked filters = A([]);
@tracked softFilters = A([]);
@ -312,6 +311,18 @@ export default class MembersController extends Controller {
});
}
@action
bulkRemoveLabel() {
this.modals.open('modals/members/bulk-remove-label', {
query: this.getApiQueryObject(),
onComplete: () => {
// reset and reload
this.store.unloadAll('member');
this.reload();
}
});
}
@action
changePaidParam(paid) {
this.paidParam = paid.value;
@ -327,11 +338,6 @@ export default class MembersController extends Controller {
this.showUnsubscribeMembersModal = !this.showUnsubscribeMembersModal;
}
@action
toggleRemoveMembersLabelModal() {
this.showRemoveMembersLabelModal = !this.showRemoveMembersLabelModal;
}
@action
deleteMembers() {
return this.deleteMembersTask.perform();
@ -342,11 +348,6 @@ export default class MembersController extends Controller {
return this.unsubscribeMembersTask.perform();
}
@action
removeLabelFromMembers(selectedLabel) {
return this.removeLabelFromMembersTask.perform(selectedLabel);
}
// Tasks -------------------------------------------------------------------
@task({restartable: true})
@ -480,29 +481,6 @@ export default class MembersController extends Controller {
return response?.bulk?.meta;
}
@task({drop: true})
*removeLabelFromMembersTask(selectedLabel) {
const query = new URLSearchParams(this.getApiQueryObject());
const removeLabelUrl = `${this.ghostPaths.url.api('members/bulk')}?${query}`;
const response = yield this.ajax.put(removeLabelUrl, {
data: {
bulk: {
action: 'removeLabel',
meta: {
label: {
id: selectedLabel
}
}
}
}
});
// reset and reload
this.store.unloadAll('member');
this.reload();
return response?.bulk?.meta;
}
// Internal ----------------------------------------------------------------
resetFilters(params) {

View File

@ -74,7 +74,7 @@
</button>
</li>
<li>
<button class="mr2" data-test-button="remove-label-selected" type="button" {{on "click" this.toggleRemoveMembersLabelModal}}>
<button class="mr2" data-test-button="remove-label-selected" type="button" {{on "click" this.bulkRemoveLabel}}>
<span>Remove label from selected members ({{this.members.length}})</span>
</button>
</li>
@ -178,16 +178,6 @@
{{outlet}}
{{#if this.showRemoveMembersLabelModal}}
<GhFullscreenModal
@modal="remove-label-members"
@model={{hash memberCount=this.members.length}}
@confirm={{this.removeLabelFromMembers}}
@close={{this.toggleRemoveMembersLabelModal}}
@modifier="action wide"
/>
{{/if}}
{{#if this.showUnsubscribeMembersModal}}
<GhFullscreenModal
@modal="unsubscribe-members"