From c902770091e0af899f9c86d1acb02be36662b083 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 20:06:20 -0500 Subject: [PATCH 01/26] build: add webpack build pipeline with newspack-scripts Co-Authored-By: Claude Opus 4.6 (1M context) --- .distignore | 4 ++++ .gitignore | 1 + package.json | 11 +++++++---- src/blocks/republish-button/index.js | 2 ++ src/blocks/republish-button/view.js | 2 ++ webpack.config.js | 12 ++++++++++++ 6 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 src/blocks/republish-button/index.js create mode 100644 src/blocks/republish-button/view.js create mode 100644 webpack.config.js diff --git a/.distignore b/.distignore index d307c4b..0a301e0 100644 --- a/.distignore +++ b/.distignore @@ -35,3 +35,7 @@ contributing.md docs release.sh release +src/**/*.js +src/**/*.jsx +src/**/*.scss +webpack.config.js diff --git a/.gitignore b/.gitignore index bda5e68..ea83f92 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules/ release/ vendor .phpunit.result.cache +dist/ diff --git a/package.json b/package.json index d021fac..fffe33e 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "main": "Gruntfile.js", "author": "Automattic", "scripts": { - "start": "npm ci", + "start": "npm ci && npm run watch", "cm": "newspack-scripts commit", - "build": "echo 'No build in this repository.'", + "build": "npm run clean && newspack-scripts wp-scripts build", + "watch": "npm run clean && newspack-scripts wp-scripts start", + "clean": "rm -rf dist", "i18n": "grunt i18n", - "lint:js": "echo 'No JS to lint in this repository.'", + "lint:js": "newspack-scripts wp-scripts lint-js 'src/**/*.{js,jsx}'", + "lint:scss": "newspack-scripts wp-scripts lint-style 'src/**/*.scss'", "readme": "grunt readme", "semantic-release": "newspack-scripts release --files=republication-tracker-tool.php", - "release:archive": "rm -rf release && mkdir -p release && rsync -r . ./release/republication-tracker-tool --exclude-from='./.distignore' && cd release && zip -r republication-tracker-tool.zip republication-tracker-tool", + "release:archive": "npm run build && rm -rf release && mkdir -p release && rsync -r . ./release/republication-tracker-tool --exclude-from='./.distignore' && cd release && zip -r republication-tracker-tool.zip republication-tracker-tool", "release": "npm run semantic-release" }, "repository": { diff --git a/src/blocks/republish-button/index.js b/src/blocks/republish-button/index.js new file mode 100644 index 0000000..ffb0d92 --- /dev/null +++ b/src/blocks/republish-button/index.js @@ -0,0 +1,2 @@ +// Placeholder for build verification. +console.log( 'republish-button' ); diff --git a/src/blocks/republish-button/view.js b/src/blocks/republish-button/view.js new file mode 100644 index 0000000..70749cd --- /dev/null +++ b/src/blocks/republish-button/view.js @@ -0,0 +1,2 @@ +// Placeholder for build verification. +console.log( 'republish-button-view' ); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..ff5770b --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,12 @@ +/** + * External dependencies + */ +const path = require( 'path' ); +const getBaseWebpackConfig = require( 'newspack-scripts/config/getWebpackConfig' ); + +const entry = { + 'republish-button': path.join( __dirname, 'src', 'blocks', 'republish-button', 'index.js' ), + 'republish-button-view': path.join( __dirname, 'src', 'blocks', 'republish-button', 'view.js' ), +}; + +module.exports = getBaseWebpackConfig( { entry } ); From d54ddb74d363a52a41d4540f330a53828003a439 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 20:16:11 -0500 Subject: [PATCH 02/26] refactor: shared modal flag and remove inline onclick from copy button Co-Authored-By: Claude Opus 4.6 (1M context) --- assets/widget.js | 8 ++++++++ includes/class-widget.php | 21 ++++++++------------- includes/shareable-content.php | 2 +- republication-tracker-tool.php | 7 +++++++ 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/assets/widget.js b/assets/widget.js index 84c4b97..a7c120f 100644 --- a/assets/widget.js +++ b/assets/widget.js @@ -96,6 +96,14 @@ function show_modal( $modal, $close ) { initTabSwitching(); + // Bind copy button via data attribute (replaces inline onclick). + var $copyBtn = $modal.find( '[data-copy-active]' ); + $copyBtn.off( 'click.republish' ).on( 'click.republish', function() { + if ( window.ClipboardUtils ) { + ClipboardUtils.copyFromElement( getActiveTextarea(), this ); + } + } ); + trapFocus( $modal ); $close.focus(); } diff --git a/includes/class-widget.php b/includes/class-widget.php index 90056cb..1517bd9 100644 --- a/includes/class-widget.php +++ b/includes/class-widget.php @@ -13,13 +13,6 @@ */ class Republication_Tracker_Tool_Widget extends WP_Widget { - /** - * Whether the widget has been instantiated. - * - * @var bool - */ - public $has_instance = false; - /** * Sets up the widgets name etc. */ @@ -108,25 +101,27 @@ public function widget( $args, $instance ) { echo wp_kses_post( $args['after_widget'] ); - // if has_instance is false, we can continue with displaying the modal. - if ( isset( $this->has_instance ) && false === $this->has_instance ) { + // If the modal has not been rendered yet, include it. + if ( ! Republication_Tracker_Tool::$modal_rendered ) { - // update has_instance so the next time the widget is created on the same page, it does not create a second modal. - $this->has_instance = true; + // Mark the modal as rendered. + Republication_Tracker_Tool::$modal_rendered = true; // define our path to grab file content from. $modal_content_path = plugin_dir_path( __FILE__ ) . 'shareable-content.php'; + $is_amp = self::is_amp(); + if ( $is_amp ) { ?> - + - + Date: Mon, 30 Mar 2026 20:19:00 -0500 Subject: [PATCH 03/26] feat(block): add block.json metadata and base styles Co-Authored-By: Claude Opus 4.6 (1M context) --- src/blocks/republish-button/block.json | 62 ++++++++++++++++++++++++++ src/blocks/republish-button/style.scss | 13 ++++++ 2 files changed, 75 insertions(+) create mode 100644 src/blocks/republish-button/block.json create mode 100644 src/blocks/republish-button/style.scss diff --git a/src/blocks/republish-button/block.json b/src/blocks/republish-button/block.json new file mode 100644 index 0000000..45342e5 --- /dev/null +++ b/src/blocks/republish-button/block.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "republication-tracker-tool/republish-button", + "title": "Republish Button", + "category": "widgets", + "description": "A button that allows readers to republish your content under a Creative Commons license.", + "keywords": [ "republish", "creative commons", "syndication" ], + "textdomain": "republication-tracker-tool", + "attributes": { + "buttonText": { + "type": "string", + "default": "Republish This Story" + }, + "message": { + "type": "string", + "default": "Republish our articles for free, online or in print, under a Creative Commons license." + }, + "displayMode": { + "type": "string", + "default": "modal", + "enum": [ "modal", "page" ] + } + }, + "supports": { + "anchor": true, + "color": { + "background": true, + "text": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalLetterSpacing": true + }, + "spacing": { + "padding": true, + "margin": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "style": true, + "width": true + }, + "shadow": true + }, + "editorScript": "file:../../../dist/republish-button.js", + "editorStyle": "file:../../../dist/republish-button.css", + "style": "file:../../../dist/republish-button.css", + "example": { + "attributes": { + "buttonText": "Republish This Story", + "message": "Republish our articles for free, online or in print, under a Creative Commons license.", + "displayMode": "modal" + } + } +} diff --git a/src/blocks/republish-button/style.scss b/src/blocks/republish-button/style.scss new file mode 100644 index 0000000..b6c7b51 --- /dev/null +++ b/src/blocks/republish-button/style.scss @@ -0,0 +1,13 @@ +.wp-block-republication-tracker-tool-republish-button { + &__button { + display: inline-block; + cursor: pointer; + text-decoration: none; + border: none; + font: inherit; + } + + &__message { + margin-bottom: 0.5em; + } +} From a7a3530e1f2ceed2d44506650e33c2b51321f901 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 20:22:43 -0500 Subject: [PATCH 04/26] feat(block): add editor component with block registration Co-Authored-By: Claude Opus 4.6 (1M context) --- src/blocks/republish-button/edit.js | 82 ++++++++++++++++++++++++++++ src/blocks/republish-button/index.js | 23 +++++++- 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 src/blocks/republish-button/edit.js diff --git a/src/blocks/republish-button/edit.js b/src/blocks/republish-button/edit.js new file mode 100644 index 0000000..70433b3 --- /dev/null +++ b/src/blocks/republish-button/edit.js @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + InspectorControls, + RichText, + useBlockProps, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseBorderProps as useBorderProps, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalUseColorProps as useColorProps, + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalGetSpacingClassesAndStyles as useSpacingProps, +} from '@wordpress/block-editor'; +import { PanelBody, SelectControl } from '@wordpress/components'; + +function RepublishButtonEdit( { attributes, setAttributes } ) { + const { buttonText, message, displayMode } = attributes; + + const colorProps = useColorProps( attributes ); + const borderProps = useBorderProps( attributes ); + const spacingProps = useSpacingProps( attributes ); + + const blockProps = useBlockProps(); + + const buttonClasses = classnames( + 'wp-block-republication-tracker-tool-republish-button__button', + colorProps.className, + borderProps.className + ); + + const buttonStyles = { + ...borderProps.style, + ...colorProps.style, + ...spacingProps.style, + }; + + return ( + <> + + + setAttributes( { displayMode: val } ) } + /> + + +
+ setAttributes( { message: val } ) } + placeholder={ __( 'Add a description of the republish feature...', 'republication-tracker-tool' ) } + withoutInteractiveFormatting + /> + + setAttributes( { buttonText: val } ) } + placeholder={ __( 'Republish This Story', 'republication-tracker-tool' ) } + allowedFormats={ [] } + /> + +
+ + ); +} + +export default RepublishButtonEdit; diff --git a/src/blocks/republish-button/index.js b/src/blocks/republish-button/index.js index ffb0d92..ea561d0 100644 --- a/src/blocks/republish-button/index.js +++ b/src/blocks/republish-button/index.js @@ -1,2 +1,21 @@ -// Placeholder for build verification. -console.log( 'republish-button' ); +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; +import './style.scss'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + edit: Edit, + save: () => null, +}; + +registerBlockType( { name, ...metadata }, settings ); From f9feea182b20d7068f9da717f71d5a02708e1f5f Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 20:26:14 -0500 Subject: [PATCH 05/26] feat(block): add PHP block class with server-side rendering Co-Authored-By: Claude Opus 4.6 (1M context) --- includes/class-republish-button-block.php | 175 ++++++++++++++++++++++ republication-tracker-tool.php | 1 + 2 files changed, 176 insertions(+) create mode 100644 includes/class-republish-button-block.php diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php new file mode 100644 index 0000000..336859e --- /dev/null +++ b/includes/class-republish-button-block.php @@ -0,0 +1,175 @@ + [ __CLASS__, 'render_block' ], + ] + ); + } + + /** + * Render the block on the frontend. + * + * @param array $attrs Block attributes. + * @return string Rendered block HTML. + */ + public static function render_block( $attrs ) { + global $post; + + // Guard: only render on singular views. + if ( ! is_singular() ) { + return ''; + } + + // Guard: check allowed post types. + $allowed_post_types = apply_filters( 'republication_tracker_tool_post_types', [ 'post' ] ); + if ( ! in_array( get_post_type(), $allowed_post_types, true ) ) { + return ''; + } + + // Guard: check if widget is hidden for this post (same filter as the widget). + $hide = apply_filters( + 'hide_republication_widget', + get_post_meta( $post->ID, 'republication-tracker-tool-hide-widget', true ), + $post + ); + if ( true == $hide ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual + return ''; + } + + // Translated defaults (block.json defaults are not translatable). + $default_attrs = [ + 'buttonText' => __( 'Republish This Story', 'republication-tracker-tool' ), + 'message' => __( 'Republish our articles for free, online or in print, under a Creative Commons license.', 'republication-tracker-tool' ), + 'displayMode' => 'modal', + ]; + $attrs = wp_parse_args( $attrs, $default_attrs ); + + // Fall back to translated default when attribute is empty string. + $button_text = '' === trim( (string) $attrs['buttonText'] ) ? $default_attrs['buttonText'] : $attrs['buttonText']; + $message_text = '' === trim( (string) $attrs['message'] ) ? $default_attrs['message'] : $attrs['message']; + $display_mode = $attrs['displayMode']; + + // License badge. + $license_key = get_option( 'republication_tracker_tool_license', REPUBLICATION_TRACKER_TOOL_DEFAULT_LICENSE ); + $using_license = isset( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ] ); + + // Build the CTA element with block support attributes. + $extra_classes = [ 'wp-block-republication-tracker-tool-republish-button__button' ]; + $wrapper_args = [ 'class' => implode( ' ', $extra_classes ) ]; + $wrapper_attributes = get_block_wrapper_attributes( $wrapper_args ); + + // Start building output. + $html = '
'; + + // Message. + $html .= '

' . wp_kses_post( $message_text ) . '

'; + + // Button or link. + if ( 'page' === $display_mode ) { + $endpoint = apply_filters( 'republication_tracker_tool_endpoint', 'republish' ); + $republish_url = home_url( '/' . $endpoint . wp_make_link_relative( get_permalink( $post->ID ) ) ); + $html .= '' . esc_html( $button_text ) . ''; + } else { + $html .= ''; + } + + // License badge. + if ( $using_license ) { + $html .= sprintf( + '

%s

', + esc_url( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ]['url'] ), + esc_html__( 'Creative Commons License', 'republication-tracker-tool' ), + esc_url( plugin_dir_url( __DIR__ ) ) . 'assets/img/' . esc_attr( $license_key ) . '.png' + ); + } + + $html .= '
'; + + // Modal (only for modal mode, only once per page). + if ( 'modal' === $display_mode ) { + self::enqueue_modal_assets(); + + if ( ! Republication_Tracker_Tool::$modal_rendered ) { + Republication_Tracker_Tool::$modal_rendered = true; + + $is_amp = false; // Block themes do not support AMP. + $modal_content_path = REPUBLICATION_TRACKER_TOOL_PATH . 'includes/shareable-content.php'; + + ob_start(); + ?> + + [], 'version' => REPUBLICATION_TRACKER_TOOL_VERSION ]; + + wp_enqueue_script( + 'republication-tracker-tool-republish-button-view', + REPUBLICATION_TRACKER_TOOL_URL . 'dist/republish-button-view.js', + array_merge( $asset['dependencies'], [ 'republication-tracker-tool-clipboard-utils' ] ), + $asset['version'], + true + ); + } +} + +Republication_Tracker_Tool_Republish_Button_Block::init(); diff --git a/republication-tracker-tool.php b/republication-tracker-tool.php index 1752f9f..d5f977e 100644 --- a/republication-tracker-tool.php +++ b/republication-tracker-tool.php @@ -32,6 +32,7 @@ function_exists( 'get_plugin_data' ) || require_once ABSPATH . 'wp-admin/include require plugin_dir_path( __FILE__ ) . 'includes/class-widget.php'; require plugin_dir_path( __FILE__ ) . 'includes/compatibility-co-authors-plus.php'; require plugin_dir_path( __FILE__ ) . 'includes/class-republication-rewrite.php'; +require plugin_dir_path( __FILE__ ) . 'includes/class-republish-button-block.php'; /** * Main initiation class. From 395099be0c3eb814753e0cf7f963ecbad17b73e2 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 20:28:57 -0500 Subject: [PATCH 06/26] feat(block): add frontend modal JS for block trigger Co-Authored-By: Claude Opus 4.6 (1M context) --- src/blocks/republish-button/view.js | 235 +++++++++++++++++++++++++++- 1 file changed, 233 insertions(+), 2 deletions(-) diff --git a/src/blocks/republish-button/view.js b/src/blocks/republish-button/view.js index 70749cd..42bc6ec 100644 --- a/src/blocks/republish-button/view.js +++ b/src/blocks/republish-button/view.js @@ -1,2 +1,233 @@ -// Placeholder for build verification. -console.log( 'republish-button-view' ); +/** + * WordPress dependencies + */ +import domReady from '@wordpress/dom-ready'; + +/** + * Get the textarea for the currently active tab. + * + * @return {HTMLElement|null} The active textarea element. + */ +function getActiveTextarea() { + const activeTextarea = document.querySelector( + '.republish-content.republish-content--active textarea' + ); + if ( activeTextarea ) { + return activeTextarea; + } + // Fallback to the original textarea if no tabs are present. + return document.querySelector( + '#republication-tracker-tool-shareable-content' + ); +} + +/** + * Initialize tab switching between HTML and Plain Text formats. + * + * @param {HTMLElement} modal The modal element. + */ +function initTabSwitching( modal ) { + const tabButtons = modal.querySelectorAll( + '.republish-format-tabs__button' + ); + const tabContents = modal.querySelectorAll( '.republish-content' ); + const mainCopyButton = modal.querySelector( + '.republication-tracker-tool__copy-button--main' + ); + + tabButtons.forEach( ( button ) => { + button.addEventListener( 'click', ( e ) => { + e.preventDefault(); + const targetTab = button.getAttribute( 'data-tab' ); + + // Update active states. + tabButtons.forEach( ( btn ) => + btn.classList.remove( 'republish-format-tabs__button--active' ) + ); + tabContents.forEach( ( content ) => + content.classList.remove( 'republish-content--active' ) + ); + + button.classList.add( 'republish-format-tabs__button--active' ); + const targetContent = modal.querySelector( + `[data-tab-content="${ targetTab }"]` + ); + if ( targetContent ) { + targetContent.classList.add( 'republish-content--active' ); + } + + // Show/hide main copy button based on active tab. + if ( mainCopyButton ) { + if ( targetTab === 'html' ) { + mainCopyButton.classList.add( 'show-for-html' ); + } else { + mainCopyButton.classList.remove( 'show-for-html' ); + } + } + } ); + } ); + + // Initialize copy buttons for individual plain text fields. + modal.querySelectorAll( '.plain-text-field__button' ).forEach( ( btn ) => { + btn.addEventListener( 'click', ( e ) => { + e.preventDefault(); + const target = btn.getAttribute( 'data-target' ); + if ( window.ClipboardUtils && target ) { + ClipboardUtils.copyFromElement( target, btn ); + } + } ); + } ); + + // Bind main copy button via data attribute. + const copyActiveBtn = modal.querySelector( '[data-copy-active]' ); + if ( copyActiveBtn ) { + copyActiveBtn.addEventListener( 'click', ( e ) => { + e.preventDefault(); + if ( window.ClipboardUtils ) { + ClipboardUtils.copyFromElement( getActiveTextarea(), copyActiveBtn ); + } + } ); + } +} + +/** + * Strip captions from shareable content. + * + * @param {HTMLElement} modal The modal element. + */ +function stripCaptions( modal ) { + const shareable = modal.querySelector( + '#republication-tracker-tool-shareable-content' + ); + if ( ! shareable ) { + return; + } + const html = shareable.textContent; + const parser = new DOMParser(); + const doc = parser.parseFromString( html, 'text/html' ); + doc.querySelectorAll( '.wp-caption' ).forEach( ( el ) => el.remove() ); + shareable.innerHTML = doc.body.innerHTML; +} + +/** + * Trap focus within the modal for accessibility. + * + * @param {HTMLElement} modal The modal element. + */ +function trapFocus( modal ) { + const focusableSelector = + 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), select:not([disabled])'; + const focusableEls = modal.querySelectorAll( focusableSelector ); + if ( ! focusableEls.length ) { + return; + } + const firstFocusable = focusableEls[ 0 ]; + const lastFocusable = focusableEls[ focusableEls.length - 1 ]; + + modal.addEventListener( 'keydown', ( e ) => { + if ( e.key !== 'Tab' ) { + return; + } + if ( e.shiftKey ) { + if ( document.activeElement === firstFocusable ) { + lastFocusable.focus(); + e.preventDefault(); + } + } else if ( document.activeElement === lastFocusable ) { + firstFocusable.focus(); + e.preventDefault(); + } + } ); +} + +/** + * Show the modal. + * + * @param {HTMLElement} modal The modal element. + * @param {HTMLElement} triggerButton The button that triggered the modal. + */ +function showModal( modal, triggerButton ) { + const modalContent = modal.querySelector( + '#republication-tracker-tool-modal-content' + ); + + // Move modal to body if not already there. + if ( modal.parentNode !== document.body ) { + document.body.appendChild( modal ); + } + + modal.style.display = ''; + if ( modalContent ) { + modalContent.style.display = ''; + } + document.body.classList.add( 'modal-open-disallow-scrolling' ); + + // Prevent clicks inside modal content from closing modal. + if ( modalContent ) { + modalContent.addEventListener( 'click', ( e ) => e.stopPropagation() ); + } + + stripCaptions( modal ); + initTabSwitching( modal ); + trapFocus( modal ); + + // Focus close button. + const closeBtn = modal.querySelector( '.republication-tracker-tool-close' ); + if ( closeBtn ) { + closeBtn.focus(); + } + + // Close handlers. + const closeModal = () => { + document.body.classList.remove( 'modal-open-disallow-scrolling' ); + modal.style.display = 'none'; + if ( triggerButton ) { + triggerButton.focus(); + } + }; + + // Click outside modal content. + modal.addEventListener( 'click', closeModal ); + + // Close button. + if ( closeBtn ) { + closeBtn.addEventListener( 'click', ( e ) => { + e.stopPropagation(); + closeModal(); + } ); + } + + // Escape key. + const escHandler = ( e ) => { + if ( e.key === 'Escape' ) { + closeModal(); + document.removeEventListener( 'keyup', escHandler ); + } + }; + document.addEventListener( 'keyup', escHandler ); +} + +domReady( () => { + const modal = document.getElementById( 'republication-tracker-tool-modal' ); + if ( ! modal ) { + return; + } + + // Find all block trigger buttons. + document + .querySelectorAll( '[data-modal-trigger="republish"]' ) + .forEach( ( button ) => { + button.addEventListener( 'click', ( e ) => { + e.preventDefault(); + showModal( modal, button ); + } ); + } ); + + // Auto-open via URL hash. + if ( window.location.hash === '#show-republish' ) { + const firstTrigger = document.querySelector( + '[data-modal-trigger="republish"]' + ); + showModal( modal, firstTrigger ); + } +} ); From 162cbb99b59bda36894d899640e1ee324cb99aa4 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 20:32:23 -0500 Subject: [PATCH 07/26] chore: raise minimum WP version to 6.0 for block support Co-Authored-By: Claude Opus 4.6 (1M context) --- readme.txt | 4 ++-- republication-tracker-tool.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/readme.txt b/readme.txt index b4e4152..1d847ed 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: innlabs Donate link: https://inn.org/donate Tags: publishers, news -Requires at least: 5.3 +Requires at least: 6.0 Requires PHP: 7.4 -Tested up to: 6.4.3 +Tested up to: 7.0 Stable tag: trunk License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html diff --git a/republication-tracker-tool.php b/republication-tracker-tool.php index d5f977e..824a70c 100644 --- a/republication-tracker-tool.php +++ b/republication-tracker-tool.php @@ -6,6 +6,7 @@ * Author URI: https://labs.inn.org * Text Domain: republication-tracker-tool * Domain Path: /languages + * Requires at least: 6.0 * Version: 2.8.1 * * @package Republication_Tracker_Tool From 262254e02e1d79f8eebea5e4e8d5cc4e1b748435 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 20:51:29 -0500 Subject: [PATCH 08/26] fix(block): address review findings from 12 parallel code reviewers Co-Authored-By: Claude Opus 4.6 (1M context) --- includes/class-republish-button-block.php | 14 ++- src/blocks/republish-button/edit.js | 1 + src/blocks/republish-button/style.scss | 5 + src/blocks/republish-button/view.js | 107 ++++++++++++++-------- 4 files changed, 86 insertions(+), 41 deletions(-) diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php index 336859e..6939626 100644 --- a/includes/class-republish-button-block.php +++ b/includes/class-republish-button-block.php @@ -5,6 +5,9 @@ * @package Republication_Tracker_Tool */ +// Exit if accessed directly. +defined( 'ABSPATH' ) || exit; + /** * Republish Button Block class. */ @@ -43,13 +46,13 @@ public static function render_block( $attrs ) { global $post; // Guard: only render on singular views. - if ( ! is_singular() ) { + if ( ! is_singular() || ! $post instanceof \WP_Post ) { return ''; } // Guard: check allowed post types. $allowed_post_types = apply_filters( 'republication_tracker_tool_post_types', [ 'post' ] ); - if ( ! in_array( get_post_type(), $allowed_post_types, true ) ) { + if ( ! in_array( get_post_type( $post ), $allowed_post_types, true ) ) { return ''; } @@ -71,6 +74,11 @@ public static function render_block( $attrs ) { ]; $attrs = wp_parse_args( $attrs, $default_attrs ); + // Validate displayMode against allowed values. + if ( ! in_array( $attrs['displayMode'], [ 'modal', 'page' ], true ) ) { + $attrs['displayMode'] = 'modal'; + } + // Fall back to translated default when attribute is empty string. $button_text = '' === trim( (string) $attrs['buttonText'] ) ? $default_attrs['buttonText'] : $attrs['buttonText']; $message_text = '' === trim( (string) $attrs['message'] ) ? $default_attrs['message'] : $attrs['message']; @@ -106,7 +114,7 @@ public static function render_block( $attrs ) { '

%s

', esc_url( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ]['url'] ), esc_html__( 'Creative Commons License', 'republication-tracker-tool' ), - esc_url( plugin_dir_url( __DIR__ ) ) . 'assets/img/' . esc_attr( $license_key ) . '.png' + esc_url( plugin_dir_url( __DIR__ ) . 'assets/img/' . $license_key . '.png' ) ); } diff --git a/src/blocks/republish-button/edit.js b/src/blocks/republish-button/edit.js index 70433b3..4231476 100644 --- a/src/blocks/republish-button/edit.js +++ b/src/blocks/republish-button/edit.js @@ -72,6 +72,7 @@ function RepublishButtonEdit( { attributes, setAttributes } ) { onChange={ ( val ) => setAttributes( { buttonText: val } ) } placeholder={ __( 'Republish This Story', 'republication-tracker-tool' ) } allowedFormats={ [] } + aria-label={ __( 'Button text', 'republication-tracker-tool' ) } /> diff --git a/src/blocks/republish-button/style.scss b/src/blocks/republish-button/style.scss index b6c7b51..7eb96c3 100644 --- a/src/blocks/republish-button/style.scss +++ b/src/blocks/republish-button/style.scss @@ -1,10 +1,15 @@ .wp-block-republication-tracker-tool-republish-button { + &__button { display: inline-block; cursor: pointer; text-decoration: none; border: none; + padding: 0; + background: none; + color: inherit; font: inherit; + line-height: inherit; } &__message { diff --git a/src/blocks/republish-button/view.js b/src/blocks/republish-button/view.js index 42bc6ec..6dadef0 100644 --- a/src/blocks/republish-button/view.js +++ b/src/blocks/republish-button/view.js @@ -3,6 +3,9 @@ */ import domReady from '@wordpress/dom-ready'; +let initialized = false; +let currentTrigger = null; + /** * Get the textarea for the currently active tab. * @@ -141,70 +144,98 @@ function trapFocus( modal ) { } /** - * Show the modal. + * Close the modal and return focus to the trigger button. * - * @param {HTMLElement} modal The modal element. - * @param {HTMLElement} triggerButton The button that triggered the modal. + * @param {HTMLElement} modal The modal element. */ -function showModal( modal, triggerButton ) { +function closeModal( modal ) { + document.body.classList.remove( 'modal-open-disallow-scrolling' ); + modal.style.display = 'none'; + if ( currentTrigger ) { + currentTrigger.focus(); + } + currentTrigger = null; +} + +/** + * Initialize modal event listeners once. + * + * @param {HTMLElement} modal The modal element. + */ +function initModal( modal ) { + if ( initialized ) { + return; + } + initialized = true; + const modalContent = modal.querySelector( '#republication-tracker-tool-modal-content' ); + const closeBtn = modal.querySelector( '.republication-tracker-tool-close' ); - // Move modal to body if not already there. + // Move modal to body once. if ( modal.parentNode !== document.body ) { document.body.appendChild( modal ); } - modal.style.display = ''; - if ( modalContent ) { - modalContent.style.display = ''; - } - document.body.classList.add( 'modal-open-disallow-scrolling' ); - - // Prevent clicks inside modal content from closing modal. - if ( modalContent ) { - modalContent.addEventListener( 'click', ( e ) => e.stopPropagation() ); - } - + // Strip captions once (not per-open). stripCaptions( modal ); + + // Tab switching (bind once). initTabSwitching( modal ); + + // Focus trap (bind once). trapFocus( modal ); - // Focus close button. - const closeBtn = modal.querySelector( '.republication-tracker-tool-close' ); - if ( closeBtn ) { - closeBtn.focus(); + // Prevent clicks inside modal content from closing modal. + if ( modalContent ) { + modalContent.addEventListener( 'click', ( e ) => e.stopPropagation() ); } - // Close handlers. - const closeModal = () => { - document.body.classList.remove( 'modal-open-disallow-scrolling' ); - modal.style.display = 'none'; - if ( triggerButton ) { - triggerButton.focus(); - } - }; - - // Click outside modal content. - modal.addEventListener( 'click', closeModal ); + // Click outside modal content closes modal. + modal.addEventListener( 'click', () => closeModal( modal ) ); // Close button. if ( closeBtn ) { closeBtn.addEventListener( 'click', ( e ) => { e.stopPropagation(); - closeModal(); + closeModal( modal ); } ); } // Escape key. - const escHandler = ( e ) => { - if ( e.key === 'Escape' ) { - closeModal(); - document.removeEventListener( 'keyup', escHandler ); + document.addEventListener( 'keydown', ( e ) => { + if ( e.key === 'Escape' && modal.style.display !== 'none' ) { + closeModal( modal ); } - }; - document.addEventListener( 'keyup', escHandler ); + } ); +} + +/** + * Show the modal. + * + * @param {HTMLElement} modal The modal element. + * @param {HTMLElement} triggerButton The button that triggered the modal. + */ +function showModal( modal, triggerButton ) { + initModal( modal ); + currentTrigger = triggerButton; + + const modalContent = modal.querySelector( + '#republication-tracker-tool-modal-content' + ); + + modal.style.display = ''; + if ( modalContent ) { + modalContent.style.display = ''; + } + document.body.classList.add( 'modal-open-disallow-scrolling' ); + + // Focus close button. + const closeBtn = modal.querySelector( '.republication-tracker-tool-close' ); + if ( closeBtn ) { + closeBtn.focus(); + } } domReady( () => { From 1aace67b840bea700f027205e92522a532c59755 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 21:01:24 -0500 Subject: [PATCH 09/26] fix(block): address remaining review findings Co-Authored-By: Claude Opus 4.6 (1M context) --- includes/class-republish-button-block.php | 2 +- package-lock.json | 4 +++- package.json | 3 +++ src/blocks/republish-button/block.json | 5 ++--- src/blocks/republish-button/edit.js | 5 ++--- src/blocks/republish-button/index.js | 9 ++------- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php index 6939626..0bafd6e 100644 --- a/includes/class-republish-button-block.php +++ b/includes/class-republish-button-block.php @@ -51,7 +51,7 @@ public static function render_block( $attrs ) { } // Guard: check allowed post types. - $allowed_post_types = apply_filters( 'republication_tracker_tool_post_types', [ 'post' ] ); + $allowed_post_types = (array) apply_filters( 'republication_tracker_tool_post_types', [ 'post' ] ); if ( ! in_array( get_post_type( $post ), $allowed_post_types, true ) ) { return ''; } diff --git a/package-lock.json b/package-lock.json index c1d0d91..0bbf603 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "republication-tracker-tool", "version": "2.8.1", "license": "GPL-2.0-or-later", + "dependencies": { + "classnames": "^2.5.1" + }, "devDependencies": { "grunt": "~1.6.1", "grunt-wp-i18n": "~1.0.3", @@ -12013,7 +12016,6 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "dev": true, "license": "MIT" }, "node_modules/clean-stack": { diff --git a/package.json b/package.json index fffe33e..afc9fc5 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,8 @@ "grunt-wp-i18n": "~1.0.3", "grunt-wp-readme-to-markdown": "~2.1.0", "newspack-scripts": "^5.9.5" + }, + "dependencies": { + "classnames": "^2.5.1" } } diff --git a/src/blocks/republish-button/block.json b/src/blocks/republish-button/block.json index 45342e5..b644c49 100644 --- a/src/blocks/republish-button/block.json +++ b/src/blocks/republish-button/block.json @@ -35,11 +35,11 @@ "__experimentalFontWeight": true, "__experimentalFontStyle": true, "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, "__experimentalLetterSpacing": true }, "spacing": { - "padding": true, - "margin": true + "padding": true }, "__experimentalBorder": { "radius": true, @@ -50,7 +50,6 @@ "shadow": true }, "editorScript": "file:../../../dist/republish-button.js", - "editorStyle": "file:../../../dist/republish-button.css", "style": "file:../../../dist/republish-button.css", "example": { "attributes": { diff --git a/src/blocks/republish-button/edit.js b/src/blocks/republish-button/edit.js index 4231476..9c80ac0 100644 --- a/src/blocks/republish-button/edit.js +++ b/src/blocks/republish-button/edit.js @@ -11,12 +11,11 @@ import { InspectorControls, RichText, useBlockProps, - // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + /* eslint-disable @wordpress/no-unsafe-wp-apis */ __experimentalUseBorderProps as useBorderProps, - // eslint-disable-next-line @wordpress/no-unsafe-wp-apis __experimentalUseColorProps as useColorProps, - // eslint-disable-next-line @wordpress/no-unsafe-wp-apis __experimentalGetSpacingClassesAndStyles as useSpacingProps, + /* eslint-enable @wordpress/no-unsafe-wp-apis */ } from '@wordpress/block-editor'; import { PanelBody, SelectControl } from '@wordpress/components'; diff --git a/src/blocks/republish-button/index.js b/src/blocks/republish-button/index.js index ea561d0..213009b 100644 --- a/src/blocks/republish-button/index.js +++ b/src/blocks/republish-button/index.js @@ -10,12 +10,7 @@ import metadata from './block.json'; import Edit from './edit'; import './style.scss'; -const { name } = metadata; -export { metadata, name }; - -export const settings = { +registerBlockType( metadata, { edit: Edit, save: () => null, -}; - -registerBlockType( { name, ...metadata }, settings ); +} ); From 73620aa109d3043ca0e1bccba63cb4ccd80677f6 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 21:18:50 -0500 Subject: [PATCH 10/26] fix(block): apply wrapper attributes to outer div for correct anchor and layout support Co-Authored-By: Claude Opus 4.6 (1M context) --- includes/class-republish-button-block.php | 15 +++++------ package-lock.json | 4 +-- package.json | 3 --- src/blocks/republish-button/edit.js | 31 +++-------------------- 4 files changed, 12 insertions(+), 41 deletions(-) diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php index 0bafd6e..8b66158 100644 --- a/includes/class-republish-button-block.php +++ b/includes/class-republish-button-block.php @@ -88,24 +88,23 @@ public static function render_block( $attrs ) { $license_key = get_option( 'republication_tracker_tool_license', REPUBLICATION_TRACKER_TOOL_DEFAULT_LICENSE ); $using_license = isset( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ] ); - // Build the CTA element with block support attributes. - $extra_classes = [ 'wp-block-republication-tracker-tool-republish-button__button' ]; - $wrapper_args = [ 'class' => implode( ' ', $extra_classes ) ]; - $wrapper_attributes = get_block_wrapper_attributes( $wrapper_args ); + // Block wrapper attributes go on the outer div (anchor, alignment, layout, block supports). + $wrapper_attributes = get_block_wrapper_attributes(); // Start building output. - $html = '
'; + $html = '
'; // Message. $html .= '

' . wp_kses_post( $message_text ) . '

'; - // Button or link. + // Button or link (inherits colors from wrapper via CSS). + $button_class = 'wp-block-republication-tracker-tool-republish-button__button'; if ( 'page' === $display_mode ) { $endpoint = apply_filters( 'republication_tracker_tool_endpoint', 'republish' ); $republish_url = home_url( '/' . $endpoint . wp_make_link_relative( get_permalink( $post->ID ) ) ); - $html .= '' . esc_html( $button_text ) . ''; + $html .= '' . esc_html( $button_text ) . ''; } else { - $html .= ''; + $html .= ''; } // License badge. diff --git a/package-lock.json b/package-lock.json index 0bbf603..c1d0d91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "republication-tracker-tool", "version": "2.8.1", "license": "GPL-2.0-or-later", - "dependencies": { - "classnames": "^2.5.1" - }, "devDependencies": { "grunt": "~1.6.1", "grunt-wp-i18n": "~1.0.3", @@ -12016,6 +12013,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "dev": true, "license": "MIT" }, "node_modules/clean-stack": { diff --git a/package.json b/package.json index afc9fc5..fffe33e 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,5 @@ "grunt-wp-i18n": "~1.0.3", "grunt-wp-readme-to-markdown": "~2.1.0", "newspack-scripts": "^5.9.5" - }, - "dependencies": { - "classnames": "^2.5.1" } } diff --git a/src/blocks/republish-button/edit.js b/src/blocks/republish-button/edit.js index 9c80ac0..629caf1 100644 --- a/src/blocks/republish-button/edit.js +++ b/src/blocks/republish-button/edit.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -11,35 +6,17 @@ import { InspectorControls, RichText, useBlockProps, - /* eslint-disable @wordpress/no-unsafe-wp-apis */ - __experimentalUseBorderProps as useBorderProps, - __experimentalUseColorProps as useColorProps, - __experimentalGetSpacingClassesAndStyles as useSpacingProps, - /* eslint-enable @wordpress/no-unsafe-wp-apis */ } from '@wordpress/block-editor'; import { PanelBody, SelectControl } from '@wordpress/components'; function RepublishButtonEdit( { attributes, setAttributes } ) { const { buttonText, message, displayMode } = attributes; - const colorProps = useColorProps( attributes ); - const borderProps = useBorderProps( attributes ); - const spacingProps = useSpacingProps( attributes ); - + // Block supports (color, typography, spacing, border, shadow) are applied + // to the wrapper div automatically by useBlockProps(). The inner button + // inherits colors via CSS. const blockProps = useBlockProps(); - const buttonClasses = classnames( - 'wp-block-republication-tracker-tool-republish-button__button', - colorProps.className, - borderProps.className - ); - - const buttonStyles = { - ...borderProps.style, - ...colorProps.style, - ...spacingProps.style, - }; - return ( <> @@ -64,7 +41,7 @@ function RepublishButtonEdit( { attributes, setAttributes } ) { placeholder={ __( 'Add a description of the republish feature...', 'republication-tracker-tool' ) } withoutInteractiveFormatting /> - + Date: Mon, 30 Mar 2026 23:11:01 -0500 Subject: [PATCH 11/26] fix: resolve PHPCS, eslint plugin, and test failures Co-Authored-By: Claude Opus 4.6 (1M context) --- includes/class-republish-button-block.php | 5 +- package-lock.json | 243 +++++++++++++++------- package.json | 1 + tests/test-widget.php | 1 + 4 files changed, 179 insertions(+), 71 deletions(-) diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php index 8b66158..77e5205 100644 --- a/includes/class-republish-button-block.php +++ b/includes/class-republish-button-block.php @@ -167,7 +167,10 @@ private static function enqueue_modal_assets() { $asset_file = REPUBLICATION_TRACKER_TOOL_PATH . 'dist/republish-button-view.asset.php'; $asset = file_exists( $asset_file ) ? include $asset_file - : [ 'dependencies' => [], 'version' => REPUBLICATION_TRACKER_TOOL_VERSION ]; + : [ + 'dependencies' => [], + 'version' => REPUBLICATION_TRACKER_TOOL_VERSION, + ]; wp_enqueue_script( 'republication-tracker-tool-republish-button-view', diff --git a/package-lock.json b/package-lock.json index c1d0d91..a922a31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "2.8.1", "license": "GPL-2.0-or-later", "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.58.0", "grunt": "~1.6.1", "grunt-wp-i18n": "~1.0.3", "grunt-wp-readme-to-markdown": "~2.1.0", @@ -2850,9 +2851,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7318,18 +7319,82 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", - "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7339,20 +7404,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", - "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7362,18 +7427,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", - "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7384,9 +7449,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", - "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", "dev": true, "license": "MIT", "engines": { @@ -7397,7 +7462,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { @@ -7606,9 +7671,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", "dev": true, "license": "MIT", "engines": { @@ -7620,22 +7685,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", - "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.2", - "@typescript-eslint/tsconfig-utils": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7645,13 +7709,52 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -7662,16 +7765,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", - "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7681,19 +7784,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", - "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -15190,13 +15293,13 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -32665,9 +32768,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index fffe33e..142bde4 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "url": "https://github.com/Automattic/republication-tracker-tool/issues" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.58.0", "grunt": "~1.6.1", "grunt-wp-i18n": "~1.0.3", "grunt-wp-readme-to-markdown": "~2.1.0", diff --git a/tests/test-widget.php b/tests/test-widget.php index 043d61b..f48658d 100644 --- a/tests/test-widget.php +++ b/tests/test-widget.php @@ -46,6 +46,7 @@ public function set_up() { */ public function tear_down() { wp_delete_post( $this->test_post->ID, true ); + Republication_Tracker_Tool::$modal_rendered = false; parent::tear_down(); } From 529045fd9579633497ab02dc9864168d3d2cf8de Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Mon, 30 Mar 2026 23:11:11 -0500 Subject: [PATCH 12/26] fix(block): address Copilot review feedback Co-Authored-By: Claude Opus 4.6 (1M context) --- includes/class-republish-button-block.php | 20 ++- tests/test-republish-button-block.php | 194 ++++++++++++++++++++++ 2 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 tests/test-republish-button-block.php diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php index 77e5205..555fc7f 100644 --- a/includes/class-republish-button-block.php +++ b/includes/class-republish-button-block.php @@ -21,18 +21,26 @@ public static function init() { } /** - * Register the block. Only available on block themes. + * Register the block. + * + * On block themes the block is fully available. On classic themes it is + * still registered (so existing content does not become an "unknown block") + * but hidden from the inserter. */ public static function register_block() { - if ( ! function_exists( 'wp_is_block_theme' ) || ! wp_is_block_theme() ) { - return; + $args = [ + 'render_callback' => [ __CLASS__, 'render_block' ], + ]; + + if ( function_exists( 'wp_is_block_theme' ) && ! wp_is_block_theme() ) { + $args['supports'] = [ + 'inserter' => false, + ]; } register_block_type_from_metadata( REPUBLICATION_TRACKER_TOOL_PATH . 'src/blocks/republish-button', - [ - 'render_callback' => [ __CLASS__, 'render_block' ], - ] + $args ); } diff --git a/tests/test-republish-button-block.php b/tests/test-republish-button-block.php new file mode 100644 index 0000000..1c170bf --- /dev/null +++ b/tests/test-republish-button-block.php @@ -0,0 +1,194 @@ +test_post = $this->factory->post->create_and_get( + [ + 'post_title' => 'Test Post for Block', + 'post_content' => '

Test content for block display.

', + 'post_status' => 'publish', + ] + ); + } + + /** + * Clean up after tests. + */ + public function tear_down() { + wp_delete_post( $this->test_post->ID, true ); + Republication_Tracker_Tool::$modal_rendered = false; + parent::tear_down(); + } + + /** + * Helper to set up a singular post context. + */ + private function set_singular_context() { + global $post, $wp_query; + $post = $this->test_post; + $wp_query->is_singular = true; + $wp_query->is_single = true; + $wp_query->queried_object = $this->test_post; + $wp_query->queried_object_id = $this->test_post->ID; + } + + /** + * Test block renders on singular post view. + */ + public function test_block_renders_on_singular_post() { + $this->set_singular_context(); + + $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + + $this->assertStringContainsString( 'wp-block-republication-tracker-tool-republish-button', $output ); + $this->assertStringContainsString( 'Republish This Story', $output ); + $this->assertStringContainsString( 'data-modal-trigger="republish"', $output ); + } + + /** + * Test block returns empty on non-singular views. + */ + public function test_block_empty_on_non_singular() { + global $wp_query; + $wp_query->is_singular = false; + $wp_query->is_single = false; + + $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + + $this->assertEmpty( $output ); + } + + /** + * Test block respects post type filter. + */ + public function test_block_respects_post_type_filter() { + $this->set_singular_context(); + + add_filter( + 'republication_tracker_tool_post_types', + function () { + return [ 'page' ]; + } + ); + + $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + + $this->assertEmpty( $output ); + + remove_all_filters( 'republication_tracker_tool_post_types' ); + } + + /** + * Test block respects hide widget meta. + */ + public function test_block_respects_hide_meta() { + $this->set_singular_context(); + update_post_meta( $this->test_post->ID, 'republication-tracker-tool-hide-widget', true ); + + $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + + $this->assertEmpty( $output ); + + delete_post_meta( $this->test_post->ID, 'republication-tracker-tool-hide-widget' ); + } + + /** + * Test block respects hide_republication_widget filter. + */ + public function test_block_respects_hide_filter() { + $this->set_singular_context(); + add_filter( 'hide_republication_widget', '__return_true' ); + + $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + + $this->assertEmpty( $output ); + + remove_filter( 'hide_republication_widget', '__return_true' ); + } + + /** + * Test modal is only rendered once across multiple blocks. + */ + public function test_single_modal_across_multiple_blocks() { + $this->set_singular_context(); + + $output1 = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + $output2 = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + + $combined = $output1 . $output2; + + $this->assertEquals( 1, substr_count( $combined, 'republication-tracker-tool-modal' ), 'Single modal in combined output.' ); + $this->assertEquals( 2, substr_count( $combined, 'data-modal-trigger="republish"' ), 'Two trigger buttons in combined output.' ); + } + + /** + * Test page mode renders a link instead of a button. + */ + public function test_page_mode_renders_link() { + $this->set_singular_context(); + + $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( + [ + 'displayMode' => 'page', + ] + ); + + $this->assertStringContainsString( 'assertStringNotContainsString( 'data-modal-trigger', $output ); + $this->assertStringNotContainsString( 'republication-tracker-tool-modal', $output ); + } + + /** + * Test empty attributes fall back to translated defaults. + */ + public function test_empty_attributes_fallback() { + $this->set_singular_context(); + + $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( + [ + 'buttonText' => '', + 'message' => '', + ] + ); + + $this->assertStringContainsString( 'Republish This Story', $output ); + $this->assertStringContainsString( 'Republish our articles for free', $output ); + } + + /** + * Test invalid displayMode falls back to modal. + */ + public function test_invalid_display_mode_fallback() { + $this->set_singular_context(); + + $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( + [ + 'displayMode' => 'invalid', + ] + ); + + $this->assertStringContainsString( 'data-modal-trigger="republish"', $output ); + } +} From 642d9b87f909e1e6bf6a10044eacb5fd9239be5d Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Tue, 31 Mar 2026 00:46:45 -0500 Subject: [PATCH 13/26] fix: resolve JS lint errors in edit.js and view.js Co-Authored-By: Claude Opus 4.6 (1M context) --- src/blocks/republish-button/edit.js | 51 ++++++++++++++++++++++++----- src/blocks/republish-button/view.js | 12 +++++-- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/blocks/republish-button/edit.js b/src/blocks/republish-button/edit.js index 629caf1..5ccb4bd 100644 --- a/src/blocks/republish-button/edit.js +++ b/src/blocks/republish-button/edit.js @@ -20,15 +20,37 @@ function RepublishButtonEdit( { attributes, setAttributes } ) { return ( <> - + setAttributes( { displayMode: val } ) } + onChange={ ( val ) => + setAttributes( { displayMode: val } ) + } /> @@ -38,17 +60,28 @@ function RepublishButtonEdit( { attributes, setAttributes } ) { className="wp-block-republication-tracker-tool-republish-button__message" value={ message } onChange={ ( val ) => setAttributes( { message: val } ) } - placeholder={ __( 'Add a description of the republish feature...', 'republication-tracker-tool' ) } + placeholder={ __( + 'Add a description of the republish feature…', + 'republication-tracker-tool' + ) } withoutInteractiveFormatting /> setAttributes( { buttonText: val } ) } - placeholder={ __( 'Republish This Story', 'republication-tracker-tool' ) } + onChange={ ( val ) => + setAttributes( { buttonText: val } ) + } + placeholder={ __( + 'Republish This Story', + 'republication-tracker-tool' + ) } allowedFormats={ [] } - aria-label={ __( 'Button text', 'republication-tracker-tool' ) } + aria-label={ __( + 'Button text', + 'republication-tracker-tool' + ) } />
diff --git a/src/blocks/republish-button/view.js b/src/blocks/republish-button/view.js index 6dadef0..8509959 100644 --- a/src/blocks/republish-button/view.js +++ b/src/blocks/republish-button/view.js @@ -1,3 +1,5 @@ +/* global ClipboardUtils, DOMParser */ + /** * WordPress dependencies */ @@ -87,7 +89,10 @@ function initTabSwitching( modal ) { copyActiveBtn.addEventListener( 'click', ( e ) => { e.preventDefault(); if ( window.ClipboardUtils ) { - ClipboardUtils.copyFromElement( getActiveTextarea(), copyActiveBtn ); + ClipboardUtils.copyFromElement( + getActiveTextarea(), + copyActiveBtn + ); } } ); } @@ -131,12 +136,13 @@ function trapFocus( modal ) { if ( e.key !== 'Tab' ) { return; } + const active = modal.ownerDocument.activeElement; if ( e.shiftKey ) { - if ( document.activeElement === firstFocusable ) { + if ( active === firstFocusable ) { lastFocusable.focus(); e.preventDefault(); } - } else if ( document.activeElement === lastFocusable ) { + } else if ( active === lastFocusable ) { firstFocusable.focus(); e.preventDefault(); } From f228d76153f80f2ac2e7c107981bce0b366bf952 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Tue, 31 Mar 2026 00:46:57 -0500 Subject: [PATCH 14/26] fix: update widget test assertion and register block in test setup Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test-republish-button-block.php | 10 ++++++++++ tests/test-widget.php | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test-republish-button-block.php b/tests/test-republish-button-block.php index 1c170bf..7d7218a 100644 --- a/tests/test-republish-button-block.php +++ b/tests/test-republish-button-block.php @@ -23,6 +23,16 @@ class RepublishButtonBlockTest extends WP_UnitTestCase { public function set_up() { parent::set_up(); + // Register the block type so get_block_wrapper_attributes() works. + if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'republication-tracker-tool/republish-button' ) ) { + register_block_type_from_metadata( + REPUBLICATION_TRACKER_TOOL_PATH . 'src/blocks/republish-button', + [ + 'render_callback' => [ 'Republication_Tracker_Tool_Republish_Button_Block', 'render_block' ], + ] + ); + } + $this->test_post = $this->factory->post->create_and_get( [ 'post_title' => 'Test Post for Block', diff --git a/tests/test-widget.php b/tests/test-widget.php index f48658d..b0053b5 100644 --- a/tests/test-widget.php +++ b/tests/test-widget.php @@ -157,11 +157,11 @@ public function test_multiple_widget_instances() { $this->widget->widget( $args, $instance ); $this->widget->widget( $args, $instance ); $output = ob_get_clean(); - $this->assertStringContainsString( 'republication-tracker-tool-modal', $output ); + $this->assertStringContainsString( 'id="republication-tracker-tool-modal"', $output ); $this->assertStringContainsString( 'republication-tracker-tool-button', $output ); - // Check only one modal is present. - $this->assertEquals( substr_count( $output, 'republication-tracker-tool-modal' ), 1, 'Single modal found in output.' ); - $this->assertEquals( substr_count( $output, 'republication-tracker-tool-button' ), 3, 'Multiple buttons found in output.' ); + // Check only one modal wrapper is present. + $this->assertEquals( 1, substr_count( $output, 'id="republication-tracker-tool-modal"' ), 'Single modal found in output.' ); + $this->assertEquals( 3, substr_count( $output, 'republication-tracker-tool-button' ), 'Multiple buttons found in output.' ); } } From 1e18f1c00187f9b0c43c457599edc1df2fb9f2c6 Mon Sep 17 00:00:00 2001 From: Ramon Corrales Date: Tue, 31 Mar 2026 01:02:09 -0500 Subject: [PATCH 15/26] fix: resolve test failures and load composer autoloader in bootstrap Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/bootstrap.php | 3 +++ tests/test-republish-button-block.php | 37 +++++++++++++++------------ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 123187e..deca9e2 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,6 +5,9 @@ * @package Creative_Commons_Sharing */ +// Load the composer autoloader. +require_once __DIR__ . '/../vendor/autoload.php'; + $_tests_dir = getenv( 'WP_TESTS_DIR' ); if ( ! $_tests_dir ) { $_tests_dir = '/tmp/wordpress-tests-lib'; diff --git a/tests/test-republish-button-block.php b/tests/test-republish-button-block.php index 7d7218a..d1f7e3b 100644 --- a/tests/test-republish-button-block.php +++ b/tests/test-republish-button-block.php @@ -63,13 +63,26 @@ private function set_singular_context() { $wp_query->queried_object_id = $this->test_post->ID; } + /** + * Helper to render the block through the standard pipeline so + * WP_Block_Supports context is set correctly. + * + * @param array $attrs Block attributes. + * @return string Rendered HTML. + */ + private function render_block( $attrs = [] ) { + $json = empty( $attrs ) ? '' : ' ' . wp_json_encode( (object) $attrs ); + $serialized = ''; + return do_blocks( $serialized ); + } + /** * Test block renders on singular post view. */ public function test_block_renders_on_singular_post() { $this->set_singular_context(); - $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + $output = $this->render_block(); $this->assertStringContainsString( 'wp-block-republication-tracker-tool-republish-button', $output ); $this->assertStringContainsString( 'Republish This Story', $output ); @@ -143,12 +156,12 @@ public function test_block_respects_hide_filter() { public function test_single_modal_across_multiple_blocks() { $this->set_singular_context(); - $output1 = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); - $output2 = Republication_Tracker_Tool_Republish_Button_Block::render_block( [] ); + $output1 = $this->render_block(); + $output2 = $this->render_block(); $combined = $output1 . $output2; - $this->assertEquals( 1, substr_count( $combined, 'republication-tracker-tool-modal' ), 'Single modal in combined output.' ); + $this->assertEquals( 1, substr_count( $combined, 'id="republication-tracker-tool-modal"' ), 'Single modal in combined output.' ); $this->assertEquals( 2, substr_count( $combined, 'data-modal-trigger="republish"' ), 'Two trigger buttons in combined output.' ); } @@ -158,16 +171,12 @@ public function test_single_modal_across_multiple_blocks() { public function test_page_mode_renders_link() { $this->set_singular_context(); - $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( - [ - 'displayMode' => 'page', - ] - ); + $output = $this->render_block( [ 'displayMode' => 'page' ] ); $this->assertStringContainsString( 'assertStringNotContainsString( 'data-modal-trigger', $output ); - $this->assertStringNotContainsString( 'republication-tracker-tool-modal', $output ); + $this->assertStringNotContainsString( 'id="republication-tracker-tool-modal"', $output ); } /** @@ -176,7 +185,7 @@ public function test_page_mode_renders_link() { public function test_empty_attributes_fallback() { $this->set_singular_context(); - $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( + $output = $this->render_block( [ 'buttonText' => '', 'message' => '', @@ -193,11 +202,7 @@ public function test_empty_attributes_fallback() { public function test_invalid_display_mode_fallback() { $this->set_singular_context(); - $output = Republication_Tracker_Tool_Republish_Button_Block::render_block( - [ - 'displayMode' => 'invalid', - ] - ); + $output = $this->render_block( [ 'displayMode' => 'invalid' ] ); $this->assertStringContainsString( 'data-modal-trigger="republish"', $output ); } From e900d806cc08a5e37521b415f7279740935a3dd9 Mon Sep 17 00:00:00 2001 From: Laurel Fulford Date: Tue, 12 May 2026 11:27:54 -0700 Subject: [PATCH 16/26] fix: remove 'page' as option and leave only modal --- includes/class-republish-button-block.php | 49 ++++++++--------------- src/blocks/republish-button/block.json | 8 +--- src/blocks/republish-button/edit.js | 44 +------------------- tests/test-republish-button-block.php | 25 ------------ 4 files changed, 20 insertions(+), 106 deletions(-) diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php index 555fc7f..ac31927 100644 --- a/includes/class-republish-button-block.php +++ b/includes/class-republish-button-block.php @@ -76,21 +76,14 @@ public static function render_block( $attrs ) { // Translated defaults (block.json defaults are not translatable). $default_attrs = [ - 'buttonText' => __( 'Republish This Story', 'republication-tracker-tool' ), - 'message' => __( 'Republish our articles for free, online or in print, under a Creative Commons license.', 'republication-tracker-tool' ), - 'displayMode' => 'modal', + 'buttonText' => __( 'Republish This Story', 'republication-tracker-tool' ), + 'message' => __( 'Republish our articles for free, online or in print, under a Creative Commons license.', 'republication-tracker-tool' ), ]; $attrs = wp_parse_args( $attrs, $default_attrs ); - // Validate displayMode against allowed values. - if ( ! in_array( $attrs['displayMode'], [ 'modal', 'page' ], true ) ) { - $attrs['displayMode'] = 'modal'; - } - // Fall back to translated default when attribute is empty string. $button_text = '' === trim( (string) $attrs['buttonText'] ) ? $default_attrs['buttonText'] : $attrs['buttonText']; $message_text = '' === trim( (string) $attrs['message'] ) ? $default_attrs['message'] : $attrs['message']; - $display_mode = $attrs['displayMode']; // License badge. $license_key = get_option( 'republication_tracker_tool_license', REPUBLICATION_TRACKER_TOOL_DEFAULT_LICENSE ); @@ -105,15 +98,9 @@ public static function render_block( $attrs ) { // Message. $html .= '

' . wp_kses_post( $message_text ) . '

'; - // Button or link (inherits colors from wrapper via CSS). + // Modal trigger button (inherits colors from wrapper via CSS). $button_class = 'wp-block-republication-tracker-tool-republish-button__button'; - if ( 'page' === $display_mode ) { - $endpoint = apply_filters( 'republication_tracker_tool_endpoint', 'republish' ); - $republish_url = home_url( '/' . $endpoint . wp_make_link_relative( get_permalink( $post->ID ) ) ); - $html .= '
' . esc_html( $button_text ) . ''; - } else { - $html .= ''; - } + $html .= ''; // License badge. if ( $using_license ) { @@ -127,24 +114,22 @@ public static function render_block( $attrs ) { $html .= '
'; - // Modal (only for modal mode, only once per page). - if ( 'modal' === $display_mode ) { - self::enqueue_modal_assets(); + // Modal markup — only rendered once per page across all block instances. + self::enqueue_modal_assets(); - if ( ! Republication_Tracker_Tool::$modal_rendered ) { - Republication_Tracker_Tool::$modal_rendered = true; + if ( ! Republication_Tracker_Tool::$modal_rendered ) { + Republication_Tracker_Tool::$modal_rendered = true; - $is_amp = false; // Block themes do not support AMP. - $modal_content_path = REPUBLICATION_TRACKER_TOOL_PATH . 'includes/shareable-content.php'; + $is_amp = false; // Used by shareable-content.php; block themes do not support AMP. + $modal_content_path = REPUBLICATION_TRACKER_TOOL_PATH . 'includes/shareable-content.php'; - ob_start(); - ?> - - + + - - - - setAttributes( { displayMode: val } ) - } - /> - -
assertEquals( 2, substr_count( $combined, 'data-modal-trigger="republish"' ), 'Two trigger buttons in combined output.' ); } - /** - * Test page mode renders a link instead of a button. - */ - public function test_page_mode_renders_link() { - $this->set_singular_context(); - - $output = $this->render_block( [ 'displayMode' => 'page' ] ); - - $this->assertStringContainsString( 'assertStringNotContainsString( 'data-modal-trigger', $output ); - $this->assertStringNotContainsString( 'id="republication-tracker-tool-modal"', $output ); - } - /** * Test empty attributes fall back to translated defaults. */ @@ -195,15 +181,4 @@ public function test_empty_attributes_fallback() { $this->assertStringContainsString( 'Republish This Story', $output ); $this->assertStringContainsString( 'Republish our articles for free', $output ); } - - /** - * Test invalid displayMode falls back to modal. - */ - public function test_invalid_display_mode_fallback() { - $this->set_singular_context(); - - $output = $this->render_block( [ 'displayMode' => 'invalid' ] ); - - $this->assertStringContainsString( 'data-modal-trigger="republish"', $output ); - } } From b0cf32d3ece3f2a862796ee84969ef45cc84eb0d Mon Sep 17 00:00:00 2001 From: Laurel Fulford Date: Wed, 13 May 2026 14:15:22 -0700 Subject: [PATCH 17/26] feat(block): introduce republish-section pattern + license block Co-Authored-By: Claude Opus 4.7 (1M context) --- includes/class-republish-button-block.php | 45 ++++-------- includes/class-republish-license-block.php | 64 ++++++++++++++++++ includes/class-republish-pattern.php | 71 +++++++++++++++++++ republication-tracker-tool.php | 2 + src/blocks/republish-button/block.json | 8 +-- src/blocks/republish-button/edit.js | 77 ++++++++++++--------- src/blocks/republish-button/style.scss | 20 ++---- src/blocks/republish-license/block.json | 18 +++++ src/blocks/republish-license/edit.js | 28 ++++++++ src/blocks/republish-license/index.js | 15 ++++ tests/test-republish-button-block.php | 34 +++++++++- tests/test-republish-license-block.php | 79 ++++++++++++++++++++++ tests/test-republish-pattern.php | 62 +++++++++++++++++ webpack.config.js | 3 +- 14 files changed, 437 insertions(+), 89 deletions(-) create mode 100644 includes/class-republish-license-block.php create mode 100644 includes/class-republish-pattern.php create mode 100644 src/blocks/republish-license/block.json create mode 100644 src/blocks/republish-license/edit.js create mode 100644 src/blocks/republish-license/index.js create mode 100644 tests/test-republish-license-block.php create mode 100644 tests/test-republish-pattern.php diff --git a/includes/class-republish-button-block.php b/includes/class-republish-button-block.php index ac31927..82e39f8 100644 --- a/includes/class-republish-button-block.php +++ b/includes/class-republish-button-block.php @@ -77,41 +77,26 @@ public static function render_block( $attrs ) { // Translated defaults (block.json defaults are not translatable). $default_attrs = [ 'buttonText' => __( 'Republish This Story', 'republication-tracker-tool' ), - 'message' => __( 'Republish our articles for free, online or in print, under a Creative Commons license.', 'republication-tracker-tool' ), ]; $attrs = wp_parse_args( $attrs, $default_attrs ); // Fall back to translated default when attribute is empty string. - $button_text = '' === trim( (string) $attrs['buttonText'] ) ? $default_attrs['buttonText'] : $attrs['buttonText']; - $message_text = '' === trim( (string) $attrs['message'] ) ? $default_attrs['message'] : $attrs['message']; - - // License badge. - $license_key = get_option( 'republication_tracker_tool_license', REPUBLICATION_TRACKER_TOOL_DEFAULT_LICENSE ); - $using_license = isset( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ] ); - - // Block wrapper attributes go on the outer div (anchor, alignment, layout, block supports). - $wrapper_attributes = get_block_wrapper_attributes(); - - // Start building output. - $html = '
'; - - // Message. - $html .= '

' . wp_kses_post( $message_text ) . '

'; - - // Modal trigger button (inherits colors from wrapper via CSS). - $button_class = 'wp-block-republication-tracker-tool-republish-button__button'; - $html .= ''; - - // License badge. - if ( $using_license ) { - $html .= sprintf( - '

%s

', - esc_url( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ]['url'] ), - esc_html__( 'Creative Commons License', 'republication-tracker-tool' ), - esc_url( plugin_dir_url( __DIR__ ) . 'assets/img/' . $license_key . '.png' ) - ); - } + $button_text = '' === trim( (string) $attrs['buttonText'] ) ? $default_attrs['buttonText'] : $attrs['buttonText']; + + // Block supports (color, typography, spacing, border, shadow) apply to the inner + // '; + $html .= '
'; $html .= '
'; // Modal markup — only rendered once per page across all block instances. diff --git a/includes/class-republish-license-block.php b/includes/class-republish-license-block.php new file mode 100644 index 0000000..24e4da7 --- /dev/null +++ b/includes/class-republish-license-block.php @@ -0,0 +1,64 @@ + [ __CLASS__, 'render_block' ], + ] + ); + } + + /** + * Render the block on the frontend (and in editor previews via ServerSideRender). + * + * @return string Rendered HTML, or empty string when no recognizable license is set. + */ + public static function render_block() { + $license_key = get_option( 'republication_tracker_tool_license', REPUBLICATION_TRACKER_TOOL_DEFAULT_LICENSE ); + + if ( ! isset( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ] ) ) { + return ''; + } + + $license = REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ]; + + $wrapper_attributes = get_block_wrapper_attributes(); + + return sprintf( + '
%3$s
', + $wrapper_attributes, + esc_url( $license['url'] ), + esc_attr( $license['description'] ), + esc_url( $license['badge'] ) + ); + } +} + +Republication_Tracker_Tool_Republish_License_Block::init(); diff --git a/includes/class-republish-pattern.php b/includes/class-republish-pattern.php new file mode 100644 index 0000000..9dbe14c --- /dev/null +++ b/includes/class-republish-pattern.php @@ -0,0 +1,71 @@ + __( 'Republication', 'republication-tracker-tool' ) ] + ); + + $description = __( 'Republish our articles for free, online or in print, under a Creative Commons license.', 'republication-tracker-tool' ); + $button_text = __( 'Republish This Story', 'republication-tracker-tool' ); + $button_text_enc = wp_json_encode( $button_text ); + + $content = << +
+ +

{$description}

+ + + + + +
+ +HTML; + + register_block_pattern( + self::PATTERN_NAME, + [ + 'title' => __( 'Republish Section', 'republication-tracker-tool' ), + 'description' => __( 'A paragraph, republish button, and Creative Commons license badge grouped together.', 'republication-tracker-tool' ), + 'categories' => [ self::CATEGORY_SLUG ], + 'content' => $content, + ] + ); + } +} + +Republication_Tracker_Tool_Republish_Pattern::init(); diff --git a/republication-tracker-tool.php b/republication-tracker-tool.php index 3996008..1d3db34 100644 --- a/republication-tracker-tool.php +++ b/republication-tracker-tool.php @@ -33,6 +33,8 @@ function_exists( 'get_plugin_data' ) || require_once ABSPATH . 'wp-admin/include require plugin_dir_path( __FILE__ ) . 'includes/compatibility-co-authors-plus.php'; require plugin_dir_path( __FILE__ ) . 'includes/class-republication-rewrite.php'; require plugin_dir_path( __FILE__ ) . 'includes/class-republish-button-block.php'; +require plugin_dir_path( __FILE__ ) . 'includes/class-republish-license-block.php'; +require plugin_dir_path( __FILE__ ) . 'includes/class-republish-pattern.php'; /** * Main initiation class. diff --git a/src/blocks/republish-button/block.json b/src/blocks/republish-button/block.json index 51afb36..6f8ef5c 100644 --- a/src/blocks/republish-button/block.json +++ b/src/blocks/republish-button/block.json @@ -11,14 +11,11 @@ "buttonText": { "type": "string", "default": "Republish This Story" - }, - "message": { - "type": "string", - "default": "Republish our articles for free, online or in print, under a Creative Commons license." } }, "supports": { "anchor": true, + "align": false, "color": { "background": true, "text": true @@ -48,8 +45,7 @@ "style": "file:../../../dist/republish-button.css", "example": { "attributes": { - "buttonText": "Republish This Story", - "message": "Republish our articles for free, online or in print, under a Creative Commons license." + "buttonText": "Republish This Story" } } } diff --git a/src/blocks/republish-button/edit.js b/src/blocks/republish-button/edit.js index be5f5a0..334d5e6 100644 --- a/src/blocks/republish-button/edit.js +++ b/src/blocks/republish-button/edit.js @@ -2,50 +2,61 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { RichText, useBlockProps } from '@wordpress/block-editor'; +import { + RichText, + useBlockProps, + /* eslint-disable @wordpress/no-unsafe-wp-apis */ + __experimentalUseBorderProps as useBorderProps, + __experimentalUseColorProps as useColorProps, + __experimentalGetSpacingClassesAndStyles as useSpacingProps, + /* eslint-enable @wordpress/no-unsafe-wp-apis */ +} from '@wordpress/block-editor'; function RepublishButtonEdit( { attributes, setAttributes } ) { - const { buttonText, message } = attributes; + const { buttonText } = attributes; + const borderProps = useBorderProps( attributes ); + const colorProps = useColorProps( attributes ); + const spacingProps = useSpacingProps( attributes ); - // Block supports (color, typography, spacing, border, shadow) are applied - // to the wrapper div automatically by useBlockProps(). The inner button - // inherits colors via CSS. - const blockProps = useBlockProps(); + const innerClassNames = [ + 'wp-block-button__link', + 'wp-element-button', + 'wp-block-republication-tracker-tool-republish-button', + colorProps.className, + borderProps.className, + ] + .filter( Boolean ) + .join( ' ' ); + + const blockProps = useBlockProps( { + className: innerClassNames, + style: { + ...borderProps.style, + ...colorProps.style, + ...spacingProps.style, + }, + } ); return ( - <> -
+
+
setAttributes( { message: val } ) } + tagName="button" + { ...blockProps } + value={ buttonText } + onChange={ ( val ) => setAttributes( { buttonText: val } ) } placeholder={ __( - 'Add a description of the republish feature…', + 'Republish This Story', + 'republication-tracker-tool' + ) } + allowedFormats={ [] } + aria-label={ __( + 'Button text', 'republication-tracker-tool' ) } - withoutInteractiveFormatting /> - - - setAttributes( { buttonText: val } ) - } - placeholder={ __( - 'Republish This Story', - 'republication-tracker-tool' - ) } - allowedFormats={ [] } - aria-label={ __( - 'Button text', - 'republication-tracker-tool' - ) } - /> -
- +
); } diff --git a/src/blocks/republish-button/style.scss b/src/blocks/republish-button/style.scss index 7eb96c3..a7844f8 100644 --- a/src/blocks/republish-button/style.scss +++ b/src/blocks/republish-button/style.scss @@ -1,18 +1,6 @@ +// The republish button renders with the standard `wp-block-button` / `wp-block-button__link` +// markup, so theme button styles cascade automatically. Plugin-specific overrides go on the +// `wp-block-republication-tracker-tool-republish-button` class (attached to the inner button). .wp-block-republication-tracker-tool-republish-button { - - &__button { - display: inline-block; - cursor: pointer; - text-decoration: none; - border: none; - padding: 0; - background: none; - color: inherit; - font: inherit; - line-height: inherit; - } - - &__message { - margin-bottom: 0.5em; - } + cursor: pointer; } diff --git a/src/blocks/republish-license/block.json b/src/blocks/republish-license/block.json new file mode 100644 index 0000000..f7c529f --- /dev/null +++ b/src/blocks/republish-license/block.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "republication-tracker-tool/republish-license", + "title": "Republish License", + "category": "widgets", + "description": "Displays the site's current Creative Commons license badge, linked to the license.", + "keywords": [ "republish", "creative commons", "license" ], + "textdomain": "republication-tracker-tool", + "supports": { + "align": [ "left", "center", "right" ], + "spacing": { + "margin": true, + "padding": true + } + }, + "editorScript": "file:../../../dist/republish-license.js" +} diff --git a/src/blocks/republish-license/edit.js b/src/blocks/republish-license/edit.js new file mode 100644 index 0000000..083dcb0 --- /dev/null +++ b/src/blocks/republish-license/edit.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; +import ServerSideRender from '@wordpress/server-side-render'; + +function RepublishLicenseEdit() { + const blockProps = useBlockProps(); + + return ( +
+ ( +

+ { __( + 'No Creative Commons license is configured in the Republication Tracker settings.', + 'republication-tracker-tool' + ) } +

+ ) } + /> +
+ ); +} + +export default RepublishLicenseEdit; diff --git a/src/blocks/republish-license/index.js b/src/blocks/republish-license/index.js new file mode 100644 index 0000000..66842b3 --- /dev/null +++ b/src/blocks/republish-license/index.js @@ -0,0 +1,15 @@ +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Edit from './edit'; + +registerBlockType( metadata, { + edit: Edit, + save: () => null, +} ); diff --git a/tests/test-republish-button-block.php b/tests/test-republish-button-block.php index c7f97ac..ac5b3e6 100644 --- a/tests/test-republish-button-block.php +++ b/tests/test-republish-button-block.php @@ -85,6 +85,9 @@ public function test_block_renders_on_singular_post() { $output = $this->render_block(); $this->assertStringContainsString( 'wp-block-republication-tracker-tool-republish-button', $output ); + $this->assertStringContainsString( 'wp-block-buttons', $output ); + $this->assertStringContainsString( 'wp-block-button__link', $output ); + $this->assertStringContainsString( 'wp-element-button', $output ); $this->assertStringContainsString( 'Republish This Story', $output ); $this->assertStringContainsString( 'data-modal-trigger="republish"', $output ); } @@ -166,7 +169,7 @@ public function test_single_modal_across_multiple_blocks() { } /** - * Test empty attributes fall back to translated defaults. + * Test empty buttonText falls back to translated default. */ public function test_empty_attributes_fallback() { $this->set_singular_context(); @@ -174,11 +177,36 @@ public function test_empty_attributes_fallback() { $output = $this->render_block( [ 'buttonText' => '', - 'message' => '', ] ); $this->assertStringContainsString( 'Republish This Story', $output ); - $this->assertStringContainsString( 'Republish our articles for free', $output ); + } + + /** + * Block no longer emits the message paragraph. + */ + public function test_block_does_not_emit_message_paragraph() { + $this->set_singular_context(); + + $output = $this->render_block(); + + $this->assertStringNotContainsString( 'wp-block-republication-tracker-tool-republish-button__message', $output ); + } + + /** + * Block no longer emits the inline CC license badge link. + */ + public function test_block_does_not_emit_license_badge() { + // Ensure a known license is set so the legacy code path would fire if it still existed. + update_option( 'republication_tracker_tool_license', 'cc-by-nd-4.0' ); + + $this->set_singular_context(); + + $output = $this->render_block(); + + $this->assertStringNotContainsString( 'class="license"', $output ); + + delete_option( 'republication_tracker_tool_license' ); } } diff --git a/tests/test-republish-license-block.php b/tests/test-republish-license-block.php new file mode 100644 index 0000000..a64e21a --- /dev/null +++ b/tests/test-republish-license-block.php @@ -0,0 +1,79 @@ +is_registered( 'republication-tracker-tool/republish-license' ) ) { + register_block_type_from_metadata( + REPUBLICATION_TRACKER_TOOL_PATH . 'src/blocks/republish-license', + [ + 'render_callback' => [ 'Republication_Tracker_Tool_Republish_License_Block', 'render_block' ], + ] + ); + } + } + + /** + * Helper to render the block through do_blocks(). + * + * @return string Rendered HTML. + */ + private function render_block() { + return do_blocks( '' ); + } + + /** + * Renders the CC badge anchor + image when a known license is set. + */ + public function test_renders_badge_when_license_set() { + update_option( 'republication_tracker_tool_license', 'cc-by-nd-4.0' ); + + $output = $this->render_block(); + + $this->assertStringContainsString( 'assertStringContainsString( 'rel="noreferrer license"', $output ); + $this->assertStringContainsString( 'assertStringContainsString( REPUBLICATION_TRACKER_TOOL_LICENSES['cc-by-nd-4.0']['url'], $output ); + + delete_option( 'republication_tracker_tool_license' ); + } + + /** + * Renders nothing when the license key isn't in the licenses map. + */ + public function test_renders_empty_when_license_unknown() { + update_option( 'republication_tracker_tool_license', 'not-a-real-license' ); + + $output = $this->render_block(); + + $this->assertSame( '', trim( $output ) ); + + delete_option( 'republication_tracker_tool_license' ); + } + + /** + * Output is wrapped with block wrapper attributes so block supports apply. + */ + public function test_output_uses_block_wrapper_attributes() { + update_option( 'republication_tracker_tool_license', 'cc-by-nd-4.0' ); + + $output = $this->render_block(); + + $this->assertStringContainsString( 'wp-block-republication-tracker-tool-republish-license', $output ); + + delete_option( 'republication_tracker_tool_license' ); + } +} diff --git a/tests/test-republish-pattern.php b/tests/test-republish-pattern.php new file mode 100644 index 0000000..b9dcb5d --- /dev/null +++ b/tests/test-republish-pattern.php @@ -0,0 +1,62 @@ +assertTrue( + $registry->is_registered( 'republication-tracker-tool/republish-section' ), + 'Republish section pattern should be registered.' + ); + + $pattern = $registry->get_registered( 'republication-tracker-tool/republish-section' ); + + $this->assertContains( 'republication-tracker-tool', $pattern['categories'] ); + } + + /** + * The pattern category is registered with a translatable label. + */ + public function test_pattern_category_is_registered() { + $registry = WP_Block_Pattern_Categories_Registry::get_instance(); + $categories = $registry->get_all_registered(); + + $found = false; + foreach ( $categories as $category ) { + if ( 'republication-tracker-tool' === $category['name'] ) { + $found = true; + $this->assertNotEmpty( $category['label'] ); + break; + } + } + + $this->assertTrue( $found, 'Republication pattern category should be registered.' ); + } + + /** + * The pattern content references the expected child blocks. + */ + public function test_pattern_content_includes_expected_blocks() { + $registry = WP_Block_Patterns_Registry::get_instance(); + $pattern = $registry->get_registered( 'republication-tracker-tool/republish-section' ); + + $content = $pattern['content']; + + $this->assertStringContainsString( ' HTML; + // Hide the pattern from the inserter on classic themes (matches the + // republish-button block's gating). Stays registered so existing + // instances and inter-block references keep working. + $inserter = ! function_exists( 'wp_is_block_theme' ) || wp_is_block_theme(); + register_block_pattern( self::PATTERN_NAME, [ @@ -63,6 +68,7 @@ public static function register() { 'description' => __( 'A paragraph, republish button, and Creative Commons license badge grouped together.', 'republication-tracker-tool' ), 'categories' => [ self::CATEGORY_SLUG ], 'content' => $content, + 'inserter' => $inserter, ] ); } diff --git a/package-lock.json b/package-lock.json index ee5dd39..c7212c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL-2.0-or-later", "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.58.0", + "@wordpress/icons": "^11.1.0", "grunt": "~1.6.1", "grunt-wp-i18n": "~1.0.3", "grunt-wp-readme-to-markdown": "~2.1.0", diff --git a/package.json b/package.json index 8114d92..36c448a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.58.0", + "@wordpress/icons": "^11.1.0", "grunt": "~1.6.1", "grunt-wp-i18n": "~1.0.3", "grunt-wp-readme-to-markdown": "~2.1.0", diff --git a/republication-tracker-tool.php b/republication-tracker-tool.php index 1d3db34..9233d8e 100644 --- a/republication-tracker-tool.php +++ b/republication-tracker-tool.php @@ -32,8 +32,8 @@ function_exists( 'get_plugin_data' ) || require_once ABSPATH . 'wp-admin/include require plugin_dir_path( __FILE__ ) . 'includes/class-widget.php'; require plugin_dir_path( __FILE__ ) . 'includes/compatibility-co-authors-plus.php'; require plugin_dir_path( __FILE__ ) . 'includes/class-republication-rewrite.php'; -require plugin_dir_path( __FILE__ ) . 'includes/class-republish-button-block.php'; -require plugin_dir_path( __FILE__ ) . 'includes/class-republish-license-block.php'; +require plugin_dir_path( __FILE__ ) . 'src/blocks/republish-button/class-republish-button-block.php'; +require plugin_dir_path( __FILE__ ) . 'src/blocks/republish-license/class-republish-license-block.php'; require plugin_dir_path( __FILE__ ) . 'includes/class-republish-pattern.php'; /** diff --git a/includes/class-republish-button-block.php b/src/blocks/republish-button/class-republish-button-block.php similarity index 100% rename from includes/class-republish-button-block.php rename to src/blocks/republish-button/class-republish-button-block.php diff --git a/src/blocks/republish-button/index.js b/src/blocks/republish-button/index.js index 213009b..9b1f488 100644 --- a/src/blocks/republish-button/index.js +++ b/src/blocks/republish-button/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { registerBlockType } from '@wordpress/blocks'; +import { button as icon } from '@wordpress/icons'; /** * Internal dependencies @@ -12,5 +13,6 @@ import './style.scss'; registerBlockType( metadata, { edit: Edit, + icon, save: () => null, } ); diff --git a/includes/class-republish-license-block.php b/src/blocks/republish-license/class-republish-license-block.php similarity index 58% rename from includes/class-republish-license-block.php rename to src/blocks/republish-license/class-republish-license-block.php index 24e4da7..f03ad63 100644 --- a/includes/class-republish-license-block.php +++ b/src/blocks/republish-license/class-republish-license-block.php @@ -25,13 +25,25 @@ public static function init() { /** * Register the block. + * + * On block themes the block is fully available. On classic themes it is + * still registered (so existing content does not become an "unknown block") + * but hidden from the inserter, matching the republish-button gating. */ public static function register_block() { + $args = [ + 'render_callback' => [ __CLASS__, 'render_block' ], + ]; + + if ( function_exists( 'wp_is_block_theme' ) && ! wp_is_block_theme() ) { + $args['supports'] = [ + 'inserter' => false, + ]; + } + register_block_type_from_metadata( REPUBLICATION_TRACKER_TOOL_PATH . 'src/blocks/republish-license', - [ - 'render_callback' => [ __CLASS__, 'render_block' ], - ] + $args ); } @@ -51,10 +63,18 @@ public static function render_block() { $wrapper_attributes = get_block_wrapper_attributes(); + // In the editor preview (ServerSideRender → REST block-renderer endpoint) + // neutralize the link so accidental clicks don't open the real license + // URL in a new tab. Keep the + href so the markup shape is identical. + $is_editor_preview = defined( 'REST_REQUEST' ) && REST_REQUEST; + $href = $is_editor_preview ? '#' : esc_url( $license['url'] ); + $target_attr = $is_editor_preview ? '' : ' target="_blank"'; + return sprintf( - '
%3$s
', + '
%4$s
', $wrapper_attributes, - esc_url( $license['url'] ), + $href, + $target_attr, esc_attr( $license['description'] ), esc_url( $license['badge'] ) ); diff --git a/src/blocks/republish-license/index.js b/src/blocks/republish-license/index.js index 66842b3..dadc905 100644 --- a/src/blocks/republish-license/index.js +++ b/src/blocks/republish-license/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { registerBlockType } from '@wordpress/blocks'; +import { shield as icon } from '@wordpress/icons'; /** * Internal dependencies @@ -11,5 +12,6 @@ import Edit from './edit'; registerBlockType( metadata, { edit: Edit, + icon, save: () => null, } ); From 8857acdf795b26fec274b8b887b3603f8a12290a Mon Sep 17 00:00:00 2001 From: Laurel Fulford Date: Mon, 18 May 2026 12:46:22 -0700 Subject: [PATCH 19/26] fix: remove separate license block --- includes/class-republish-pattern.php | 2 - republication-tracker-tool.php | 1 - src/blocks/republish-button/block.json | 23 ++++- .../class-republish-button-block.php | 71 ++++++++++++++- src/blocks/republish-button/edit.js | 88 +++++++++++++++---- src/blocks/republish-button/style.scss | 9 ++ src/blocks/republish-license/block.json | 18 ---- .../class-republish-license-block.php | 84 ------------------ src/blocks/republish-license/edit.js | 28 ------ src/blocks/republish-license/index.js | 17 ---- tests/test-republish-button-block.php | 40 ++++++++- tests/test-republish-license-block.php | 79 ----------------- tests/test-republish-pattern.php | 2 +- webpack.config.js | 1 - 14 files changed, 205 insertions(+), 258 deletions(-) delete mode 100644 src/blocks/republish-license/block.json delete mode 100644 src/blocks/republish-license/class-republish-license-block.php delete mode 100644 src/blocks/republish-license/edit.js delete mode 100644 src/blocks/republish-license/index.js delete mode 100644 tests/test-republish-license-block.php diff --git a/includes/class-republish-pattern.php b/includes/class-republish-pattern.php index ed27036..4da6c49 100644 --- a/includes/class-republish-pattern.php +++ b/includes/class-republish-pattern.php @@ -50,8 +50,6 @@ public static function register() { - -
HTML; diff --git a/republication-tracker-tool.php b/republication-tracker-tool.php index 9233d8e..1a13871 100644 --- a/republication-tracker-tool.php +++ b/republication-tracker-tool.php @@ -33,7 +33,6 @@ function_exists( 'get_plugin_data' ) || require_once ABSPATH . 'wp-admin/include require plugin_dir_path( __FILE__ ) . 'includes/compatibility-co-authors-plus.php'; require plugin_dir_path( __FILE__ ) . 'includes/class-republication-rewrite.php'; require plugin_dir_path( __FILE__ ) . 'src/blocks/republish-button/class-republish-button-block.php'; -require plugin_dir_path( __FILE__ ) . 'src/blocks/republish-license/class-republish-license-block.php'; require plugin_dir_path( __FILE__ ) . 'includes/class-republish-pattern.php'; /** diff --git a/src/blocks/republish-button/block.json b/src/blocks/republish-button/block.json index 6f8ef5c..93ba478 100644 --- a/src/blocks/republish-button/block.json +++ b/src/blocks/republish-button/block.json @@ -11,6 +11,10 @@ "buttonText": { "type": "string", "default": "Republish This Story" + }, + "showLicense": { + "type": "boolean", + "default": true } }, "supports": { @@ -18,7 +22,11 @@ "align": false, "color": { "background": true, - "text": true + "text": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } }, "typography": { "fontSize": true, @@ -28,16 +36,23 @@ "__experimentalFontStyle": true, "__experimentalTextTransform": true, "__experimentalTextDecoration": true, - "__experimentalLetterSpacing": true + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } }, "spacing": { - "padding": true + "padding": true, + "__experimentalDefaultControls": { + "padding": true + } }, "__experimentalBorder": { "radius": true, "color": true, "style": true, - "width": true + "width": true, + "__experimentalDefaultControls": {} }, "shadow": true }, diff --git a/src/blocks/republish-button/class-republish-button-block.php b/src/blocks/republish-button/class-republish-button-block.php index 82e39f8..d27a3f6 100644 --- a/src/blocks/republish-button/class-republish-button-block.php +++ b/src/blocks/republish-button/class-republish-button-block.php @@ -18,6 +18,31 @@ final class Republication_Tracker_Tool_Republish_Button_Block { */ public static function init() { add_action( 'init', [ __CLASS__, 'register_block' ] ); + add_action( 'enqueue_block_editor_assets', [ __CLASS__, 'enqueue_editor_data' ] ); + } + + /** + * Inject the current site's Creative Commons license data into the editor + * so the block preview can render the real badge image without a REST + * round-trip. Data is read on every page load — admin setting changes + * surface on the next editor refresh. + */ + public static function enqueue_editor_data() { + $license_key = get_option( 'republication_tracker_tool_license', REPUBLICATION_TRACKER_TOOL_DEFAULT_LICENSE ); + + $license = isset( REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ] ) + ? [ + 'url' => REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ]['url'], + 'badge' => REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ]['badge'], + 'description' => REPUBLICATION_TRACKER_TOOL_LICENSES[ $license_key ]['description'], + ] + : null; + + wp_add_inline_script( + 'republication-tracker-tool-republish-button-editor-script', + 'window.republicationTrackerToolEditor = ' . wp_json_encode( [ 'license' => $license ] ) . ';', + 'before' + ); } /** @@ -76,12 +101,14 @@ public static function render_block( $attrs ) { // Translated defaults (block.json defaults are not translatable). $default_attrs = [ - 'buttonText' => __( 'Republish This Story', 'republication-tracker-tool' ), + 'buttonText' => __( 'Republish This Story', 'republication-tracker-tool' ), + 'showLicense' => true, ]; $attrs = wp_parse_args( $attrs, $default_attrs ); // Fall back to translated default when attribute is empty string. - $button_text = '' === trim( (string) $attrs['buttonText'] ) ? $default_attrs['buttonText'] : $attrs['buttonText']; + $button_text = '' === trim( (string) $attrs['buttonText'] ) ? $default_attrs['buttonText'] : $attrs['buttonText']; + $show_license = (bool) $attrs['showLicense']; // Block supports (color, typography, spacing, border, shadow) apply to the inner //