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

Added export button to posts page with placeholder endpoint (#16456)

fixes https://github.com/TryGhost/Team/issues/2780 
refs https://github.com/TryGhost/Team/issues/2781

Adds an export button to the posts page in admin (behind feature flag). It downloads a
placeholder CSV via a real endpoint (`/posts/export`).
This commit is contained in:
Simon Backx 2023-03-21 10:24:56 +01:00 committed by GitHub
parent 2096773310
commit 0cc3164b25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 117 additions and 2 deletions

View file

@ -1,4 +1,5 @@
import Controller from '@ember/controller';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {DEFAULT_QUERY_PARAMS} from 'ghost-admin/helpers/reset-query-params';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
@ -52,6 +53,7 @@ export default class PostsController extends Controller {
@service router;
@service session;
@service store;
@service utils;
@inject config;
@ -84,6 +86,10 @@ export default class PostsController extends Controller {
return this.model;
}
get totalPosts() {
return this.model.meta?.pagination?.total ?? 0;
}
get showingAll() {
const {type, author, tag, visibility} = this;
@ -136,6 +142,17 @@ export default class PostsController extends Controller {
return authors.findBy('slug', author) || {slug: '!unknown'};
}
@action
exportData() {
let exportUrl = ghostPaths().url.api('posts/export');
// the filter and order params are set from the route to the controller via the infinity model
// we can retrieve these via the extraParams of the infinity model
let downloadParams = new URLSearchParams(this.model.extraParams);
downloadParams.set('limit', 'all');
this.utils.downloadFile(`${exportUrl}?${downloadParams.toString()}`);
}
@action
changeType(type) {
this.type = type.value;

View file

@ -58,6 +58,7 @@
@import "layouts/whatsnew.css";
@import "layouts/tags.css";
@import "layouts/members.css";
@import "layouts/posts.css";
@import "layouts/member-activity.css";
@import "layouts/error.css";
@import "layouts/apps.css";

View file

@ -0,0 +1,10 @@
.gh-post-actions-menu {
top: calc(100% + 6px);
left: auto;
right: 10px;
}
.gh-post-actions-menu.fade-out {
animation-duration: .001s;
pointer-events: none;
}

View file

@ -22,7 +22,46 @@
@onOrderChange={{this.changeOrder}}
/>
<LinkTo @route="editor.new" @model="post" class="gh-btn gh-btn-primary view-actions-top-row" data-test-new-post-button={{true}}><span>New post</span></LinkTo>
<div class="view-actions-top-row">
{{#if (feature 'makingItRain')}}
<span class="dropdown posts-actions-dropdown">
<GhDropdownButton
@dropdownName="posts-actions-menu"
@classNames="gh-btn gh-btn-icon icon-only gh-btn-action-icon"
@title="Posts Actions"
data-test-button="posts-actions"
>
<span>
{{svg-jar "settings"}}
<span class="hidden">Actions</span>
</span>
</GhDropdownButton>
<GhDropdown
@name="posts-actions-menu"
@tagName="ul"
@classNames="gh-post-actions-menu dropdown-menu dropdown-triangle-top-right"
>
<li class="{{if this.totalPosts "" "disabled"}}">
{{#if this.totalPosts}}
<button class="mr2" type="button" {{on "click" this.exportData}} data-test-button="export-posts">
{{#if this.showingAll}}
<span>Export all posts</span>
{{else}}
<span>Export selected posts ({{this.totalPosts}})</span>
{{/if}}
</button>
{{else}}
<button class="mr2" disabled="true" type="button" data-test-button="export-posts">
<span>Export selected posts (0)</span>
</button>
{{/if}}
</li>
</GhDropdown>
</span>
{{/if}}
<LinkTo @route="editor.new" @model="post" class="gh-btn gh-btn-primary" data-test-new-post-button={{true}}><span>New post</span></LinkTo>
</div>
</section>
</GhCanvasHeader>

View file

@ -9,7 +9,7 @@ const allowedIncludes = [
'email',
'tiers',
'newsletter',
'count.conversions',
'count.conversions',
'count.signups',
'count.paid_conversions',
'count.clicks',
@ -57,6 +57,35 @@ module.exports = {
}
},
exportCSV: {
options: [
'limit',
'filter',
'order'
],
headers: {
disposition: {
type: 'csv',
value() {
const datetime = (new Date()).toJSON().substring(0, 10);
return `posts.${datetime}.csv`;
}
}
},
response: {
format: 'plain'
},
permissions: {
method: 'browse'
},
validation: {},
async query(frame) {
return {
data: await postsService.export(frame)
};
}
},
read: {
options: [
'include',

View file

@ -1,6 +1,7 @@
const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:posts');
const mappers = require('./mappers');
const membersService = require('../../../../../services/members');
const papaparse = require('papaparse');
module.exports = {
async all(models, apiConfig, frame) {
@ -32,5 +33,9 @@ module.exports = {
frame.response = {
posts: [post]
};
},
exportCSV(models, apiConfig, frame) {
frame.response = papaparse.unparse(models.data);
}
};

View file

@ -26,6 +26,8 @@ module.exports = function apiRoutes() {
// ## Posts
router.get('/posts', mw.authAdminApi, http(api.posts.browse));
router.get('/posts/export', mw.authAdminApi, http(api.posts.exportCSV));
router.post('/posts', mw.authAdminApi, http(api.posts.add));
router.get('/posts/:id', mw.authAdminApi, http(api.posts.read));
router.get('/posts/slug/:slug', mw.authAdminApi, http(api.posts.read));

View file

@ -57,6 +57,18 @@ class PostsService {
return model;
}
async export() {
// Placeholder implementation
return [
{
title: 'Example',
url: 'https://example.com',
author: 'Jamie Larson',
status: 'published'
}
];
}
async getProductsFromVisibilityFilter(visibilityFilter) {
try {
const allProducts = await this.models.Product.findAll();