Skip to content

Commit 87c2ad9

Browse files
authored
Merge pull request #446 from Yoast/enrico/add-sidebar-panel
Use a sidebar panel instead of the metabox in the block editor
2 parents b425494 + 32577c3 commit 87c2ad9

15 files changed

+1235
-51
lines changed

css/duplicate-post.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,22 @@ fieldset#duplicate_post_quick_edit_fieldset label{
8989
fieldset#duplicate_post_quick_edit_fieldset a{
9090
text-decoration: underline;
9191
}
92+
93+
/* Block editor sidebar panel. */
94+
.duplicate-post-panel .duplicate-post-original-item {
95+
margin-bottom: 0;
96+
}
97+
98+
.duplicate-post-panel .duplicate-post-remove-connection-button {
99+
margin-top: 16px;
100+
width: 100%;
101+
justify-content: center;
102+
}
103+
104+
.duplicate-post-modal-buttons {
105+
display: flex;
106+
justify-content: flex-end;
107+
gap: 8px;
108+
margin-top: 16px;
109+
}
110+

js/src/duplicate-post-edit-script.js

Lines changed: 158 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,168 @@
11
/* global duplicatePost, duplicatePostNotices */
22

3+
import { useState } from 'react';
34
import { registerPlugin } from "@wordpress/plugins";
4-
import { PluginPostStatusInfo } from "@wordpress/editor";
5+
import { PluginDocumentSettingPanel, PluginPostStatusInfo } from "@wordpress/editor";
56
import { Fragment } from "@wordpress/element";
6-
import { Button } from '@wordpress/components';
7+
import { Button, ExternalLink, Modal } from '@wordpress/components';
78
import { __ } from "@wordpress/i18n";
89
import { select, subscribe, dispatch } from "@wordpress/data";
10+
import apiFetch from "@wordpress/api-fetch";
911
import { redirectOnSaveCompletion } from "./duplicate-post-functions";
1012

13+
/**
14+
* Functional component for the Duplicate Post sidebar panel.
15+
*
16+
* @returns {JSX.Element|null} The rendered panel or null.
17+
*/
18+
function DuplicatePostPanel() {
19+
const [ isConfirmOpen, setIsConfirmOpen ] = useState( false );
20+
const [ isRemoving, setIsRemoving ] = useState( false );
21+
const [ referenceRemoved, setReferenceRemoved ] = useState( false );
22+
23+
const originalItem = duplicatePost.originalItem;
24+
const isRewriting = parseInt( duplicatePost.rewriting, 10 );
25+
const showMetaBox = duplicatePost.showOriginalMetaBox && originalItem && ! referenceRemoved;
26+
27+
/**
28+
* Handles the removal of the original reference via REST API.
29+
*
30+
* @returns {void}
31+
*/
32+
const handleRemoveOriginal = async () => {
33+
setIsRemoving( true );
34+
try {
35+
await apiFetch( {
36+
path: `/duplicate-post/v1/original/${ duplicatePost.postId }`,
37+
method: 'DELETE',
38+
} );
39+
setReferenceRemoved( true );
40+
setIsConfirmOpen( false );
41+
} catch ( error ) {
42+
// eslint-disable-next-line no-console
43+
console.error( 'Failed to remove original reference:', error );
44+
dispatch( 'core/notices' ).createNotice(
45+
'error',
46+
__( 'Failed to remove the connection to the original post. Please try again.', 'duplicate-post' ),
47+
{
48+
isDismissible: true,
49+
}
50+
);
51+
} finally {
52+
setIsRemoving( false );
53+
}
54+
};
55+
56+
if ( ! showMetaBox ) {
57+
return null;
58+
}
59+
60+
return (
61+
<PluginDocumentSettingPanel
62+
name="duplicate-post-panel"
63+
title={ __( "Yoast Duplicate Post", "duplicate-post" ) }
64+
className="duplicate-post-panel"
65+
>
66+
<p className="duplicate-post-original-item">
67+
{ __( 'The original item this was copied from is:', 'duplicate-post' ) }
68+
{ ' ' }
69+
<span className="duplicate_post_original_item_title_span">
70+
{ originalItem.canEdit ? (
71+
<ExternalLink href={ originalItem.editUrl }>
72+
{ originalItem.title }
73+
</ExternalLink>
74+
) : (
75+
<ExternalLink href={ originalItem.viewUrl }>
76+
{ originalItem.title }
77+
</ExternalLink>
78+
) }
79+
</span>
80+
</p>
81+
{ ! isRewriting &&
82+
<Button
83+
variant="secondary"
84+
isDestructive
85+
onClick={ () => setIsConfirmOpen( true ) }
86+
className="duplicate-post-remove-connection-button"
87+
>
88+
{ __( "Remove connection", "duplicate-post" ) }
89+
</Button>
90+
}
91+
{ isConfirmOpen &&
92+
<Modal
93+
title={ __( "Remove connection", "duplicate-post" ) }
94+
onRequestClose={ () => setIsConfirmOpen( false ) }
95+
>
96+
<p>
97+
{ __( "Are you sure you want to remove the connection to the original post? This action cannot be undone.", "duplicate-post" ) }
98+
</p>
99+
<div className="duplicate-post-modal-buttons">
100+
<Button
101+
variant="tertiary"
102+
onClick={ () => setIsConfirmOpen( false ) }
103+
>
104+
{ __( "Cancel", "duplicate-post" ) }
105+
</Button>
106+
<Button
107+
variant="primary"
108+
isDestructive
109+
isBusy={ isRemoving }
110+
onClick={ handleRemoveOriginal }
111+
>
112+
{ __( "Remove", "duplicate-post" ) }
113+
</Button>
114+
</div>
115+
</Modal>
116+
}
117+
</PluginDocumentSettingPanel>
118+
);
119+
}
120+
121+
/**
122+
* Functional component for the Duplicate Post plugin render.
123+
*
124+
* @returns {JSX.Element|null} The rendered component or null.
125+
*/
126+
function DuplicatePostRender() {
127+
// Don't try to render anything if there is no store.
128+
if ( ! select( 'core/editor' ) || ! ( wp.editor && wp.editor.PluginPostStatusInfo ) ) {
129+
return null;
130+
}
131+
132+
const currentPostStatus = select( 'core/editor' ).getEditedPostAttribute( 'status' );
133+
134+
return (
135+
<Fragment>
136+
{ ( duplicatePost.showLinksIn.submitbox === '1' ) &&
137+
<Fragment>
138+
{ ( duplicatePost.newDraftLink !== '' && duplicatePost.showLinks.new_draft === '1' ) &&
139+
<PluginPostStatusInfo>
140+
<Button
141+
isTertiary={ true }
142+
className="dp-editor-post-copy-to-draft"
143+
href={ duplicatePost.newDraftLink }
144+
>
145+
{ __( 'Copy to a new draft', 'duplicate-post' ) }
146+
</Button>
147+
</PluginPostStatusInfo>
148+
}
149+
{ ( currentPostStatus === 'publish' && duplicatePost.rewriteAndRepublishLink !== '' && duplicatePost.showLinks.rewrite_republish === '1' ) &&
150+
<PluginPostStatusInfo>
151+
<Button
152+
isTertiary={ true }
153+
className="dp-editor-post-rewrite-republish"
154+
href={ duplicatePost.rewriteAndRepublishLink }
155+
>
156+
{ __( 'Rewrite & Republish', 'duplicate-post' ) }
157+
</Button>
158+
</PluginPostStatusInfo>
159+
}
160+
</Fragment>
161+
}
162+
<DuplicatePostPanel />
163+
</Fragment>
164+
);
165+
}
11166

12167
class DuplicatePost {
13168
constructor() {
@@ -118,53 +273,11 @@ class DuplicatePost {
118273
dispatch( 'core/editor' ).removeEditorPanel( 'post-link' );
119274
}
120275
}
121-
122-
/**
123-
* Renders the links in the PluginPostStatusInfo component.
124-
*
125-
* @returns {JSX.Element} The rendered links.
126-
*/
127-
render() {
128-
// Don't try to render anything if there is no store.
129-
if ( ! select( 'core/editor' ) || ! ( wp.editor && wp.editor.PluginPostStatusInfo ) ) {
130-
return null;
131-
}
132-
133-
const currentPostStatus = select( 'core/editor' ).getEditedPostAttribute( 'status' );
134-
135-
return (
136-
( duplicatePost.showLinksIn.submitbox === '1' ) &&
137-
<Fragment>
138-
{ ( duplicatePost.newDraftLink !== '' && duplicatePost.showLinks.new_draft === '1' ) &&
139-
<PluginPostStatusInfo>
140-
<Button
141-
isTertiary={ true }
142-
className="dp-editor-post-copy-to-draft"
143-
href={ duplicatePost.newDraftLink }
144-
>
145-
{ __( 'Copy to a new draft', 'duplicate-post' ) }
146-
</Button>
147-
</PluginPostStatusInfo>
148-
}
149-
{ ( currentPostStatus === 'publish' && duplicatePost.rewriteAndRepublishLink !== '' && duplicatePost.showLinks.rewrite_republish === '1' ) &&
150-
<PluginPostStatusInfo>
151-
<Button
152-
isTertiary={ true }
153-
className="dp-editor-post-rewrite-republish"
154-
href={ duplicatePost.rewriteAndRepublishLink }
155-
>
156-
{ __( 'Rewrite & Republish', 'duplicate-post' ) }
157-
</Button>
158-
</PluginPostStatusInfo>
159-
}
160-
</Fragment>
161-
);
162-
}
163276
}
164277

165278
const instance = new DuplicatePost();
166279
instance.handleRedirect();
167280

168281
registerPlugin( 'duplicate-post', {
169-
render: instance.render
282+
render: DuplicatePostRender
170283
} );

src/admin/options.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,10 @@ public function get_options() {
233233
'tab' => 'display',
234234
'fieldset' => 'show-original',
235235
'type' => 'checkbox',
236-
'label' => \__( 'In a metabox in the Edit screen', 'duplicate-post' ),
236+
'label' => \__( 'In a sidebar panel or in a metabox in the Edit screen', 'duplicate-post' ),
237237
'value' => 1,
238238
'description' => [
239-
\__( "You'll also be able to delete the reference to the original item with a checkbox", 'duplicate-post' ),
239+
\__( "You'll also be able to delete the reference to the original item", 'duplicate-post' ),
240240
],
241241
],
242242
'duplicate_post_show_original_column' => [

src/handlers/handler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ class Handler {
5454
*/
5555
protected $check_handler;
5656

57+
/**
58+
* The REST API handler.
59+
*
60+
* @var Rest_API_Handler
61+
*/
62+
protected $rest_api_handler;
63+
5764
/**
5865
* Initializes the class.
5966
*
@@ -68,10 +75,12 @@ public function __construct( Post_Duplicator $post_duplicator, Permissions_Helpe
6875
$this->link_handler = new Link_Handler( $this->post_duplicator, $this->permissions_helper );
6976
$this->check_handler = new Check_Changes_Handler( $this->permissions_helper );
7077
$this->save_post_handler = new Save_Post_Handler( $this->permissions_helper );
78+
$this->rest_api_handler = new Rest_API_Handler( $this->permissions_helper );
7179

7280
$this->bulk_handler->register_hooks();
7381
$this->link_handler->register_hooks();
7482
$this->check_handler->register_hooks();
7583
$this->save_post_handler->register_hooks();
84+
$this->rest_api_handler->register_hooks();
7685
}
7786
}

0 commit comments

Comments
 (0)