0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

Added automatic CSV export when bulk deleting members

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

- updated bulk destroy task to first use the current query params to fetch from the appropriate CSV export endpoint and trigger a download
  - fetches via JS and triggers download from a blob URL link instead of an iframe so that we can be sure the download is successful before we hit the bulk delete endpoint
  - works differently to user deletion download because the server is not generating an export file and saving it meaning the client has to be sure we don't delete data before it's exported
- updated copy in the confirmation modal to reflect the download behaviour
This commit is contained in:
Kevin Ansfield 2021-04-08 16:06:00 +01:00
parent 51b0cfb199
commit 733f76d571
3 changed files with 45 additions and 3 deletions

View file

@ -10,6 +10,9 @@
<strong data-test-text="delete-count">{{gh-pluralize this.model.memberCount "member"}}</strong>.
This is permanent! All Ghost data will be deleted, this will have no effect on subscriptions in Stripe.
</p>
<p>
A backup of your selection will be automatically downloaded to your device before deletion.
</p>
</div>
{{else}}
<div class="gh-content-box pa" data-test-state="delete-complete">
@ -52,7 +55,7 @@
</button>
<GhTaskButton
@buttonText="Delete members"
@buttonText="Download backup & delete members"
@successText="Deleted"
@task={{this.deleteMembersTask}}
@class="gh-btn gh-btn-red gh-btn-icon"

View file

@ -1,4 +1,6 @@
import Controller from '@ember/controller';
import config from 'ghost-admin/config/environment';
import fetch from 'fetch';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import moment from 'moment';
import {A} from '@ember/array';
@ -55,6 +57,10 @@ export default class MembersController extends Controller {
constructor() {
super(...arguments);
this._availableLabels = this.store.peekAll('label');
if (this.isTesting === undefined) {
this.isTesting = config.environment === 'test';
}
}
// Computed properties -----------------------------------------------------
@ -327,10 +333,35 @@ export default class MembersController extends Controller {
*deleteMembersTask() {
const query = new URLSearchParams(this.getApiQueryObject());
let url = `${this.ghostPaths.url.api('members')}?${query}`;
// Trigger download before deleting. Uses the CSV export endpoint but
// needs to fetch the file and trigger a download directly rather than
// via an iframe. The iframe approach can't tell us when a download has
// started/finished meaning we could end up deleting the data before exporting it
const exportUrl = ghostPaths().url.api('members/upload');
const exportParams = new URLSearchParams(this.getApiQueryObject());
exportParams.set('limit', 'all');
yield fetch(exportUrl, {method: 'GET'})
.then(res => res.blob())
.then((blob) => {
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = `members.${moment().format('YYYY-MM-DD')}.csv`;
document.body.appendChild(a);
if (!this.isTesting) {
a.click();
}
a.remove();
URL.revokeObjectURL(blobUrl);
});
// backup downloaded, continue with deletion
const deleteUrl = `${this.ghostPaths.url.api('members')}?${query}`;
// response contains details of which members failed to be deleted
let response = yield this.ajax.del(url);
const response = yield this.ajax.del(deleteUrl);
// reset and reload
this.store.unloadAll('member');

View file

@ -146,5 +146,13 @@ export default function mockMembers(server) {
server.del('/members/:id/');
server.get('/members/upload/', function () {
return new Response(200, {
'Content-Disposition': 'attachment',
filename: `members.${moment().format('YYYY-MM-DD')}.csv`,
'Content-Type': 'text/csv'
}, '');
});
mockMembersStats(server);
}