🎨 Added “Copy post link” to posts list context menu (#20760)
REF DES-321 - Added a "Copy post link" button to the context menu to copy the post URL for published posts, and a "Copy preview link" for draft and scheduled posts. --------- Co-authored-by: Kevin Ansfield <kevin@lookingsideways.co.uk>
This commit is contained in:
parent
dc7abe4712
commit
ae628d7520
@ -1,11 +1,26 @@
|
||||
<ul class="gh-posts-context-menu dropdown-menu dropdown-triangle-top-left">
|
||||
|
||||
{{#if this.canUnpublishSelection}}
|
||||
{{#if this.canCopySelection}}
|
||||
<li>
|
||||
<button class="mr2" type="button" {{on "click" this.copyPostLink}}>
|
||||
<span>{{svg-jar "link"}}Copy link to post</span>
|
||||
</button>
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
<button class="mr2" type="button" {{on "click" this.unpublishPosts}}>
|
||||
<span>{{svg-jar "undo"}}Unpublish</span>
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
{{#if this.canCopySelection}}
|
||||
<li>
|
||||
<button class="mr2" type="button" {{on "click" this.copyPreviewLink}}>
|
||||
<span>{{svg-jar "link"}}Copy preview link</span>
|
||||
</button>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if this.canFeatureSelection}}
|
||||
{{#if this.shouldFeatureSelection }}
|
||||
|
@ -3,11 +3,12 @@ import Component from '@glimmer/component';
|
||||
import DeletePostsModal from './modals/delete-posts';
|
||||
import EditPostsAccessModal from './modals/edit-posts-access';
|
||||
import UnpublishPostsModal from './modals/unpublish-posts';
|
||||
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
|
||||
import nql from '@tryghost/nql';
|
||||
import {action} from '@ember/object';
|
||||
import {capitalizeFirstLetter} from 'ghost-admin/helpers/capitalize-first-letter';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {task, timeout} from 'ember-concurrency';
|
||||
|
||||
/**
|
||||
* @tryghost/tpl doesn't work in admin yet (Safari)
|
||||
@ -43,6 +44,12 @@ const messages = {
|
||||
duplicated: {
|
||||
single: '{Type} duplicated',
|
||||
multiple: '{count} {type}s duplicated'
|
||||
},
|
||||
copiedPostUrl: {
|
||||
single: 'Post link copied'
|
||||
},
|
||||
copiedPreviewUrl: {
|
||||
single: 'Preview link copied'
|
||||
}
|
||||
};
|
||||
|
||||
@ -74,6 +81,16 @@ export default class PostsContextMenu extends Component {
|
||||
return tpl(messages[type].multiple, {count: this.selectionList.count, type: this.type, Type: capitalizeFirstLetter(this.type)});
|
||||
}
|
||||
|
||||
@action
|
||||
async copyPostLink() {
|
||||
this.menu.performTask(this.copyPostLinkTask);
|
||||
}
|
||||
|
||||
@action
|
||||
async copyPreviewLink() {
|
||||
this.menu.performTask(this.copyPreviewLinkTask);
|
||||
}
|
||||
|
||||
@action
|
||||
async featurePosts() {
|
||||
this.menu.performTask(this.featurePostsTask);
|
||||
@ -403,6 +420,22 @@ export default class PostsContextMenu extends Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
@task
|
||||
*copyPostLinkTask() {
|
||||
copyTextToClipboard(this.selectionList.availableModels[0].url);
|
||||
this.notifications.showNotification(this.#getToastMessage('copiedPostUrl'), {type: 'success'});
|
||||
yield timeout(1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
@task
|
||||
*copyPreviewLinkTask() {
|
||||
copyTextToClipboard(this.selectionList.availableModels[0].url);
|
||||
this.notifications.showNotification(this.#getToastMessage('copiedPreviewUrl'), {type: 'success'});
|
||||
yield timeout(1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
async performBulkDestroy() {
|
||||
const filter = this.selectionList.filter;
|
||||
let bulkUpdateUrl = this.ghostPaths.url.api(this.type === 'post' ? 'posts' : 'pages') + `?filter=${encodeURIComponent(filter)}`;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {Factory} from 'miragejs';
|
||||
import {dasherize} from '@ember/string';
|
||||
import {isEmpty} from '@ember/utils';
|
||||
|
||||
export default Factory.extend({
|
||||
@ -27,6 +28,7 @@ export default Factory.extend({
|
||||
return statuses[i % statuses.length];
|
||||
},
|
||||
title(i) { return `Post ${i}`; },
|
||||
slug: null,
|
||||
twitterDescription: null,
|
||||
twitterImage: null,
|
||||
twitterTitle: null,
|
||||
@ -50,5 +52,10 @@ export default Factory.extend({
|
||||
post.authors = [user];
|
||||
post.save();
|
||||
}
|
||||
|
||||
if (isEmpty(post.slug)) {
|
||||
post.slug = dasherize(post.title);
|
||||
post.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
|
||||
import sinon from 'sinon';
|
||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||
import {beforeEach, describe, it} from 'mocha';
|
||||
import {blur, click, currentURL, fillIn, find, findAll, triggerEvent, triggerKeyEvent, visit} from '@ember/test-helpers';
|
||||
@ -27,6 +28,10 @@ describe('Acceptance: Posts / Pages', function () {
|
||||
this.server.loadFixtures('configs');
|
||||
});
|
||||
|
||||
this.afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('posts', function () {
|
||||
it('redirects to signin when not authenticated', async function () {
|
||||
await invalidateSession();
|
||||
@ -250,21 +255,91 @@ describe('Acceptance: Posts / Pages', function () {
|
||||
let buttons = contextMenu.querySelectorAll('button');
|
||||
|
||||
expect(contextMenu, 'context menu').to.exist;
|
||||
expect(buttons.length, 'context menu buttons').to.equal(5);
|
||||
expect(buttons[0].innerText.trim(), 'context menu button 1').to.contain('Unpublish');
|
||||
expect(buttons[1].innerText.trim(), 'context menu button 2').to.contain('Feature'); // or Unfeature
|
||||
expect(buttons[2].innerText.trim(), 'context menu button 3').to.contain('Add a tag');
|
||||
expect(buttons[3].innerText.trim(), 'context menu button 4').to.contain('Duplicate');
|
||||
expect(buttons[4].innerText.trim(), 'context menu button 5').to.contain('Delete');
|
||||
expect(buttons.length, 'context menu buttons').to.equal(6);
|
||||
expect(buttons[0].innerText.trim(), 'context menu button 1').to.contain('Copy link to post');
|
||||
expect(buttons[1].innerText.trim(), 'context menu button 1').to.contain('Unpublish');
|
||||
expect(buttons[2].innerText.trim(), 'context menu button 2').to.contain('Feature'); // or Unfeature
|
||||
expect(buttons[3].innerText.trim(), 'context menu button 3').to.contain('Add a tag');
|
||||
expect(buttons[4].innerText.trim(), 'context menu button 4').to.contain('Duplicate');
|
||||
expect(buttons[5].innerText.trim(), 'context menu button 5').to.contain('Delete');
|
||||
|
||||
// duplicate the post
|
||||
await click(buttons[3]);
|
||||
await click(buttons[4]);
|
||||
|
||||
const posts = findAll('[data-test-post-id]');
|
||||
expect(posts.length, 'all posts count').to.equal(5);
|
||||
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
|
||||
expect(lastRequest.url, 'request url').to.match(new RegExp(`/posts/${publishedPost.id}/copy/`));
|
||||
});
|
||||
|
||||
it('can copy a post link', async function () {
|
||||
sinon.stub(navigator.clipboard, 'writeText').resolves();
|
||||
|
||||
await visit('/posts');
|
||||
|
||||
// get the post
|
||||
const post = find(`[data-test-post-id="${publishedPost.id}"]`);
|
||||
expect(post, 'post').to.exist;
|
||||
|
||||
await triggerEvent(post, 'contextmenu');
|
||||
|
||||
let contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
|
||||
|
||||
let buttons = contextMenu.querySelectorAll('button');
|
||||
|
||||
expect(contextMenu, 'context menu').to.exist;
|
||||
expect(buttons.length, 'context menu buttons').to.equal(6);
|
||||
expect(buttons[0].innerText.trim(), 'context menu button 1').to.contain('Copy link to post');
|
||||
expect(buttons[1].innerText.trim(), 'context menu button 1').to.contain('Unpublish');
|
||||
expect(buttons[2].innerText.trim(), 'context menu button 2').to.contain('Feature'); // or Unfeature
|
||||
expect(buttons[3].innerText.trim(), 'context menu button 3').to.contain('Add a tag');
|
||||
expect(buttons[4].innerText.trim(), 'context menu button 4').to.contain('Duplicate');
|
||||
expect(buttons[5].innerText.trim(), 'context menu button 5').to.contain('Delete');
|
||||
|
||||
// Copy the post link
|
||||
await click(buttons[0]);
|
||||
|
||||
// Check that the notification is displayed
|
||||
expect(find('[data-test-text="notification-content"]')).to.contain.text('Post link copied');
|
||||
|
||||
// Check that the clipboard contains the right content
|
||||
expect(navigator.clipboard.writeText.calledOnce).to.be.true;
|
||||
expect(navigator.clipboard.writeText.firstCall.args[0]).to.equal(`http://localhost:4200/${publishedPost.slug}/`);
|
||||
});
|
||||
|
||||
it('can copy a preview link', async function () {
|
||||
sinon.stub(navigator.clipboard, 'writeText').resolves();
|
||||
|
||||
await visit('/posts');
|
||||
|
||||
// get the post
|
||||
const post = find(`[data-test-post-id="${draftPost.id}"]`);
|
||||
expect(post, 'post').to.exist;
|
||||
|
||||
await triggerEvent(post, 'contextmenu');
|
||||
|
||||
let contextMenu = find('.gh-posts-context-menu'); // this is a <ul> element
|
||||
|
||||
let buttons = contextMenu.querySelectorAll('button');
|
||||
|
||||
expect(contextMenu, 'context menu').to.exist;
|
||||
expect(buttons.length, 'context menu buttons').to.equal(5);
|
||||
expect(buttons[0].innerText.trim(), 'context menu button 1').to.contain('Copy preview link');
|
||||
expect(buttons[1].innerText.trim(), 'context menu button 2').to.contain('Feature'); // or Unfeature
|
||||
expect(buttons[2].innerText.trim(), 'context menu button 3').to.contain('Add a tag');
|
||||
expect(buttons[3].innerText.trim(), 'context menu button 4').to.contain('Duplicate');
|
||||
expect(buttons[4].innerText.trim(), 'context menu button 5').to.contain('Delete');
|
||||
|
||||
// Copy the preview link
|
||||
await click(buttons[0]);
|
||||
|
||||
// Check that the notification is displayed
|
||||
expect(find('[data-test-text="notification-content"]')).to.contain.text('Preview link copied');
|
||||
|
||||
// Check that the clipboard contains the right content
|
||||
expect(navigator.clipboard.writeText.calledOnce).to.be.true;
|
||||
expect(navigator.clipboard.writeText.firstCall.args[0]).to.equal(`http://localhost:4200/p/${draftPost.uuid}/`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple posts', function () {
|
||||
|
Loading…
Reference in New Issue
Block a user