From 70c9461f35cea4b763087713dc25a0c9b397d4ef Mon Sep 17 00:00:00 2001 From: Nicolas Pays Date: Thu, 8 Oct 2026 10:46:58 +0200 Subject: [PATCH 1/4] add migration command cli from tabs block --- .plugin-data | 2 +- .wordpress-org/blueprints/blueprint.json | 2 +- CHANGELOG.md | 7 + MIGRATION.md | 86 +++++ blockparty-tabs.php | 6 + composer.json | 6 + includes/Cli/MigrateFromTabsBlockCommand.php | 259 ++++++++++++++ includes/Migration/TabsBlockMigrator.php | 342 +++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- phpunit.xml.dist | 1 + readme.txt | 9 +- src/blockparty-tabs-nav-item/block.json | 2 +- src/blockparty-tabs-nav/block.json | 2 +- src/blockparty-tabs-panel-item/block.json | 2 +- src/blockparty-tabs-panels/block.json | 2 +- src/blockparty-tabs/block.json | 2 +- tests/php/TabsBlockMigratorTest.php | 123 +++++++ 18 files changed, 848 insertions(+), 11 deletions(-) create mode 100644 MIGRATION.md create mode 100644 includes/Cli/MigrateFromTabsBlockCommand.php create mode 100644 includes/Migration/TabsBlockMigrator.php create mode 100644 tests/php/TabsBlockMigratorTest.php diff --git a/.plugin-data b/.plugin-data index 3f8d940..41ef40d 100644 --- a/.plugin-data +++ b/.plugin-data @@ -1,4 +1,4 @@ { - "version": "2.0.1", + "version": "2.1.0", "slug": "blockparty-tabs" } diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json index c0ad62b..602eacd 100644 --- a/.wordpress-org/blueprints/blueprint.json +++ b/.wordpress-org/blueprints/blueprint.json @@ -11,7 +11,7 @@ "pluginData": { "resource": "git:directory", "url": "https://github.com/BeAPI/blockparty-tabs", - "ref": "2.0.1", + "ref": "2.1.0", "refType": "tag" }, "options": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 51172a2..b89a22a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.1.0 - 2026-10-08 + +- Add WP-CLI command `wp blockparty-tabs migrate-from-tabs-block` to migrate `beapi/tabs*` content to `blockparty/tabs*` (`--dry-run`, `--post-type`, `--posts-per-page`) +- Add `MIGRATION.md` for upgrading from `beapi/tabs-block` +- Add PHPUnit coverage for the tabs block migrator +- Skip block registration when compiled `build/` assets are missing + ## 2.0.1 - 2026-09-29 - Fix full-width (`alignfull`) tabs so panel padding is reset correctly diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..b94f07e --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,86 @@ +# Migrating from `beapi/tabs-block` to `blockparty-tabs` + +This document describes how to move from the abandoned [`beapi/tabs-block`](https://github.com/BeAPI/beapi-tabs-block) package to `beapi/blockparty-tabs`. + +## Package swap + +```bash +composer remove beapi/tabs-block +composer require beapi/blockparty-tabs +``` + +Activate `blockparty-tabs` and deactivate `beapi-tabs-block` (or `tabs-block`). + +## Block model changes + +| Old | New | +|---|---| +| `beapi/tabs` | `blockparty/tabs` | +| `beapi/tabs-nav` | `blockparty/tabs-nav` | +| `beapi/tabs-nav-item` | `blockparty/tabs-nav-item` | +| `beapi/tabs-panels` | `blockparty/tabs-panels` | +| `beapi/tabs-panel-item` | `blockparty/tabs-panel-item` | +| Parent `title` (saved as `aria-label` on the root) | `ariaLabel` on `blockparty/tabs-nav` | +| CSS `.wp-block-beapi-tabs` | CSS `.wp-block-blockparty-tabs` | + +Nested structure (nav + panels, paired nav/panel items) is preserved. Icons inside tab labels keep working when `blockparty-icons` or `beapi/icon-block` is active (`blockparty_tabs_allowed_icon_blocks` filter). + +## Content migration (WP-CLI) + +WP-CLI only loads **active** plugins. If you see `'blockparty-tabs' is not a registered wp command`, activate the plugin first: + +```bash +wp plugin activate blockparty-tabs +``` + +After activating `blockparty-tabs` and updating theme allowlists: + +```bash +# Preview (current site) +wp blockparty-tabs migrate-from-tabs-block --dry-run + +# Apply on the current site +wp blockparty-tabs migrate-from-tabs-block + +# Limit post types +wp blockparty-tabs migrate-from-tabs-block --post-type=post,page +``` + +The command always targets **one site only**. On multisite, repeat it yourself with `--url=` for each site: + +```bash +wp blockparty-tabs migrate-from-tabs-block --url=https://example.com/ +``` + +The command: + +1. Scans `post_content` for `beapi/tabs*` block comments and legacy class names +2. Parses blocks, converts markup recursively, serializes back +3. Moves the parent `title` attribute to `ariaLabel` on `blockparty/tabs-nav` +4. Aligns saved HTML with blockparty-tabs `save()` output (ARIA roles, `is-active` / `is-hidden` classes) +5. Updates posts with `wp_slash()` on content +6. Logs migrated / skipped counts + +### Revisions + +Only the scanned post types are migrated (public REST post types + `wp_block` by default). **Revisions are not included** unless you pass them explicitly: + +```bash +wp blockparty-tabs migrate-from-tabs-block --post-type=revision +``` + +### Duplicate Post (Yoast) + +If [Yoast Duplicate Post](https://wordpress.org/plugins/duplicate-post/) is active, updating a Rewrite & Republish **copy** via `wp_update_post()` can abort the WP-CLI run. + +Preferred workaround: skip that plugin for the migration only: + +```bash +wp --skip-plugins=duplicate-post blockparty-tabs migrate-from-tabs-block --url=https://example.com/ +``` + +## Front-end checklist + +- Update theme allowlists from `beapi/tabs*` to `blockparty/tabs*` +- Update theme SCSS selectors from `.wp-block-beapi-tabs` to `.wp-block-blockparty-tabs` +- Smoke-test top alignment and sidebar layouts, keyboard navigation, and nested tabs diff --git a/blockparty-tabs.php b/blockparty-tabs.php index 02e0a1d..72af5fa 100644 --- a/blockparty-tabs.php +++ b/blockparty-tabs.php @@ -14,6 +14,8 @@ namespace Blockparty\Tabs; +use Blockparty\Tabs\Cli\MigrateFromTabsBlockCommand; + define( 'BLOCKPARTY_TABS_VERSION', '2.0.1' ); define( 'BLOCKPARTY_TABS_URL', plugin_dir_url( __FILE__ ) ); define( 'BLOCKPARTY_TABS_DIR', plugin_dir_path( __FILE__ ) ); @@ -174,3 +176,7 @@ function register_react_jsx_runtime( $scripts ): void { } add_action( 'wp_default_scripts', __NAMESPACE__ . '\\register_react_jsx_runtime' ); + +if ( defined( 'WP_CLI' ) && WP_CLI ) { + \WP_CLI::add_command( 'blockparty-tabs migrate-from-tabs-block', MigrateFromTabsBlockCommand::class ); +} diff --git a/composer.json b/composer.json index da67fe1..e23eb56 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "beapi/blockparty-tabs", + "version": "2.1.0", "description": "Accessible Tabs block for WordPress gutenberg.", "type": "wordpress-plugin", "keywords": ["wordpress", "plugin", "tabs", "gutenberg"], @@ -23,6 +24,11 @@ "php": "8.1.99" } }, + "autoload": { + "psr-4": { + "Blockparty\\Tabs\\": "includes/" + } + }, "require": { "php": "^8.1", "composer/installers": "^1.0 || ^2.0" diff --git a/includes/Cli/MigrateFromTabsBlockCommand.php b/includes/Cli/MigrateFromTabsBlockCommand.php new file mode 100644 index 0000000..9b4d3ae --- /dev/null +++ b/includes/Cli/MigrateFromTabsBlockCommand.php @@ -0,0 +1,259 @@ +] + * : Comma-separated list of post types. Default: any public post type that shows in the REST API, plus wp_block. + * + * [--posts-per-page=] + * : Batch size. Default: 100. + * + * ## EXAMPLES + * + * wp blockparty-tabs migrate-from-tabs-block --dry-run + * wp blockparty-tabs migrate-from-tabs-block --post-type=post,page + * wp blockparty-tabs migrate-from-tabs-block --url=https://example.com/ + * + * @subcommand migrate-from-tabs-block + * + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. + * @return void + */ + public function __invoke( $args, $assoc_args ): void { + $dry_run = (bool) WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false ); + $posts_per_page = (int) ( $assoc_args['posts-per-page'] ?? 100 ); + $post_types = $this->resolve_post_types( $assoc_args['post-type'] ?? '' ); + + if ( empty( $post_types ) ) { + WP_CLI::error( 'No post types to migrate.' ); + } + + WP_CLI::log( + sprintf( + 'Migrating site %1$d (%2$s)%3$s — post types: %4$s', + get_current_blog_id(), + home_url( '/' ), + $dry_run ? ' [dry-run]' : '', + implode( ', ', $post_types ) + ) + ); + + $migrator = new TabsBlockMigrator(); + $posts_updated = 0; + $posts_scanned = 0; + $page = 1; + $max_pages = 1; + $total_migrated = 0; + $total_skipped = 0; + + while ( $page <= $max_pages ) { + $query = new WP_Query( + [ + 'post_type' => $post_types, + 'post_status' => 'any', + 'posts_per_page' => $posts_per_page, + 'paged' => $page, + 'orderby' => 'ID', + 'order' => 'ASC', + 'ignore_sticky_posts' => true, + 'no_found_rows' => false, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ] + ); + + $max_pages = (int) $query->max_num_pages; + + if ( ! $query->have_posts() ) { + break; + } + + foreach ( $query->posts as $post ) { + ++$posts_scanned; + + if ( ! is_string( $post->post_content ) || '' === $post->post_content ) { + continue; + } + + if ( ! $this->post_has_legacy_tabs( $post->post_content ) ) { + continue; + } + + $before_migrated = $migrator->migrated; + $before_skipped = $migrator->skipped; + $new_content = $migrator->migrate_content( $post->post_content ); + + $delta_migrated = $migrator->migrated - $before_migrated; + $delta_skipped = $migrator->skipped - $before_skipped; + + if ( null === $new_content ) { + continue; + } + + $label = '[dry-run]'; + if ( ! $dry_run ) { + $status = $this->persist_post_content( (int) $post->ID, $new_content ); + if ( '' === $status ) { + continue; + } + $label = '[' . $status . ']'; + } + + $total_migrated += $delta_migrated; + $total_skipped += $delta_skipped; + ++$posts_updated; + + WP_CLI::log( + sprintf( + '%1$s post %2$d (%3$s) — blocks migrated: %4$d, skipped: %5$d', + $label, + (int) $post->ID, + $post->post_type, + $delta_migrated, + $delta_skipped + ) + ); + } + + ++$page; + wp_reset_postdata(); + } + + WP_CLI::success( + sprintf( + 'Done. Posts scanned: %1$d, posts %2$s: %3$d, tab blocks migrated: %4$d, skipped: %5$d.', + $posts_scanned, + $dry_run ? 'that would update' : 'updated', + $posts_updated, + $total_migrated, + $total_skipped + ) + ); + } + + /** + * @param string $content Post content. + * @return bool + */ + private function post_has_legacy_tabs( string $content ): bool { + return false !== strpos( $content, 'beapi/tabs' ); + } + + /** + * Save migrated content, with a content-only fallback for orphaned page templates. + * + * @param int $post_id Post ID. + * @param string $new_content Unslashed post content. + * @return string Empty on failure, `update` or `update-fallback` on success. + */ + private function persist_post_content( int $post_id, string $new_content ): string { + $updated = wp_update_post( + [ + 'ID' => $post_id, + 'post_content' => wp_slash( $new_content ), + ], + true + ); + + if ( ! is_wp_error( $updated ) ) { + return 'update'; + } + + if ( 'invalid_page_template' === $updated->get_error_code() && $this->update_post_content_fallback( $post_id, $new_content ) ) { + return 'update-fallback'; + } + + WP_CLI::warning( + sprintf( + 'Failed to update post %1$d: %2$s', + $post_id, + $updated->get_error_message() + ) + ); + + return ''; + } + + /** + * Update post_content only, bypassing page template validation. + * + * @param int $post_id Post ID. + * @param string $new_content Unslashed post content. + * @return bool + */ + private function update_post_content_fallback( int $post_id, string $new_content ): bool { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Content-only fallback when wp_update_post() rejects an orphaned _wp_page_template. + $result = $wpdb->update( + $wpdb->posts, + [ + 'post_content' => $new_content, + ], + [ + 'ID' => $post_id, + ], + [ '%s' ], + [ '%d' ] + ); + + if ( false === $result ) { + return false; + } + + clean_post_cache( $post_id ); + + return true; + } + + /** + * Resolve target post types. + * + * @param string $raw Comma-separated post types from CLI. + * @return string[] + */ + private function resolve_post_types( string $raw ): array { + if ( '' !== trim( $raw ) ) { + $types = array_filter( array_map( 'trim', explode( ',', $raw ) ) ); + + return array_values( $types ); + } + + $types = get_post_types( + [ + 'public' => true, + 'show_in_rest' => true, + ], + 'names' + ); + + $types[] = 'wp_block'; + $types = array_unique( array_values( $types ) ); + + return $types; + } +} diff --git a/includes/Migration/TabsBlockMigrator.php b/includes/Migration/TabsBlockMigrator.php new file mode 100644 index 0000000..f209ace --- /dev/null +++ b/includes/Migration/TabsBlockMigrator.php @@ -0,0 +1,342 @@ + + */ + private const BLOCK_MAP = [ + 'beapi/tabs' => 'blockparty/tabs', + 'beapi/tabs-nav' => 'blockparty/tabs-nav', + 'beapi/tabs-nav-item' => 'blockparty/tabs-nav-item', + 'beapi/tabs-panels' => 'blockparty/tabs-panels', + 'beapi/tabs-panel-item' => 'blockparty/tabs-panel-item', + ]; + + /** + * Class replacements (longest keys first). + * + * @var array + */ + private const CLASS_MAP = [ + 'wp-block-beapi-tabs-panel-item__inner' => 'wp-block-blockparty-tabs-panel-item__inner', + 'wp-block-beapi-tabs-nav-link' => 'wp-block-blockparty-tabs-nav-link', + 'wp-block-beapi-tabs-nav-item' => 'wp-block-blockparty-tabs-nav-item', + 'wp-block-beapi-tabs-panel-item' => 'wp-block-blockparty-tabs-panel-item', + 'wp-block-beapi-tabs-panels' => 'wp-block-blockparty-tabs-panels', + 'wp-block-beapi-tabs-nav' => 'wp-block-blockparty-tabs-nav', + 'wp-block-beapi-tabs' => 'wp-block-blockparty-tabs', + ]; + + /** + * Entry point: rewrite post content, or return null if nothing to do. + * + * @param string $content Post content. + * @return string|null + */ + public function migrate_content( string $content ): ?string { + if ( ! $this->content_has_legacy_tabs( $content ) ) { + return null; + } + + $new = serialize_blocks( $this->migrate_blocks( parse_blocks( $content ) ) ); + + return $new === $content ? null : $new; + } + + /** + * @param string $content Post content. + * @return bool + */ + private function content_has_legacy_tabs( string $content ): bool { + foreach ( array_keys( self::BLOCK_MAP ) as $legacy_name ) { + if ( false !== strpos( $content, $legacy_name ) ) { + return true; + } + } + + return false; + } + + /** + * Walk every block. Rename tabs blocks and fix saved markup. + * + * @param array> $blocks Parsed blocks. + * @return array> + */ + private function migrate_blocks( array $blocks ): array { + $out = []; + + foreach ( $blocks as $block ) { + $name = $block['blockName'] ?? ''; + + if ( ! empty( $block['innerBlocks'] ) ) { + $block['innerBlocks'] = $this->migrate_blocks( $block['innerBlocks'] ); + } + + if ( isset( self::BLOCK_MAP[ $name ] ) ) { + $block = $this->convert_block( $block, $name ); + ++$this->migrated; + } + + $out[] = $block; + } + + return $out; + } + + /** + * @param array $block Parsed block. + * @param string $legacy_name Legacy block name. + * @return array + */ + private function convert_block( array $block, string $legacy_name ): array { + $attrs = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : []; + $title = ''; + + if ( 'beapi/tabs' === $legacy_name ) { + $title = (string) ( $attrs['title'] ?? '' ); + unset( $attrs['title'] ); + $block['innerBlocks'] = $this->apply_title_to_nav( $block['innerBlocks'] ?? [], $title ); + } + + $block['blockName'] = self::BLOCK_MAP[ $legacy_name ]; + $block['attrs'] = $attrs; + + $this->rewrite_block_markup( $block, $block['blockName'], $attrs ); + + return $block; + } + + /** + * Move legacy parent title onto the tabs-nav block as ariaLabel. + * + * @param array> $inner_blocks Nav / panels children. + * @param string $title Parent title attribute. + * @return array> + */ + private function apply_title_to_nav( array $inner_blocks, string $title ): array { + if ( '' === $title ) { + return $inner_blocks; + } + + foreach ( $inner_blocks as $index => $inner ) { + if ( 'blockparty/tabs-nav' !== ( $inner['blockName'] ?? '' ) ) { + continue; + } + + $nav_attrs = is_array( $inner['attrs'] ?? null ) ? $inner['attrs'] : []; + if ( empty( $nav_attrs['ariaLabel'] ) ) { + $nav_attrs['ariaLabel'] = $title; + $inner['attrs'] = $nav_attrs; + $this->rewrite_block_markup( $inner, 'blockparty/tabs-nav', $nav_attrs ); + } + + $inner_blocks[ $index ] = $inner; + break; + } + + return $inner_blocks; + } + + /** + * Replace legacy classes and adjust ARIA in innerHTML / innerContent. + * + * @param array $block Parsed block (by reference). + * @param string $block_name New block name. + * @param array $attrs Block attributes. + * @return void + */ + private function rewrite_block_markup( array &$block, string $block_name, array $attrs ): void { + if ( isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ) { + $block['innerHTML'] = $this->transform_markup( $block['innerHTML'], $block_name, $attrs ); + } + + if ( empty( $block['innerContent'] ) || ! is_array( $block['innerContent'] ) ) { + return; + } + + foreach ( $block['innerContent'] as $index => $chunk ) { + if ( ! is_string( $chunk ) || '' === $chunk ) { + continue; + } + + $block['innerContent'][ $index ] = $this->transform_markup( $chunk, $block_name, $attrs ); + } + } + + /** + * @param string $html Markup fragment. + * @param string $block_name Block name after migration. + * @param array $attrs Block attributes. + * @return string + */ + private function transform_markup( string $html, string $block_name, array $attrs ): string { + $html = $this->replace_classes( $html ); + + switch ( $block_name ) { + case 'blockparty/tabs': + return $this->transform_tabs_root_markup( $html ); + case 'blockparty/tabs-nav': + return $this->transform_tabs_nav_markup( $html, $attrs ); + case 'blockparty/tabs-nav-item': + return $this->transform_tabs_nav_item_markup( $html, $attrs ); + case 'blockparty/tabs-panel-item': + return $this->transform_tabs_panel_item_markup( $html, $attrs ); + default: + return $html; + } + } + + /** + * @param string $html Markup. + * @return string + */ + private function replace_classes( string $html ): string { + return str_replace( array_keys( self::CLASS_MAP ), array_values( self::CLASS_MAP ), $html ); + } + + /** + * @param string $html Markup. + * @return string + */ + private function transform_tabs_root_markup( string $html ): string { + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + return preg_replace( '/\srole=(["\'])tablist\1/i', '', $html ) ?? $html; + } + + $processor = new \WP_HTML_Tag_Processor( $html ); + if ( $processor->next_tag() ) { + $processor->remove_attribute( 'role' ); + $processor->remove_attribute( 'aria-label' ); + } + + return $processor->get_updated_html(); + } + + /** + * @param string $html Markup. + * @param array $attrs Block attributes. + * @return string + */ + private function transform_tabs_nav_markup( string $html, array $attrs ): string { + $label = (string) ( $attrs['ariaLabel'] ?? '' ); + + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + return $html; + } + + $processor = new \WP_HTML_Tag_Processor( $html ); + if ( $processor->next_tag( [ 'tag_name' => 'UL' ] ) ) { + $processor->set_attribute( 'role', 'tablist' ); + if ( '' !== $label ) { + $processor->set_attribute( 'aria-label', $label ); + } + } + + return $processor->get_updated_html(); + } + + /** + * @param string $html Markup. + * @param array $attrs Block attributes. + * @return string + */ + private function transform_tabs_nav_item_markup( string $html, array $attrs ): string { + $index = (int) ( $attrs['index'] ?? 0 ); + $is_active = 0 === $index; + + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + return $html; + } + + $processor = new \WP_HTML_Tag_Processor( $html ); + while ( $processor->next_tag() ) { + if ( 'LI' === $processor->get_tag() ) { + $processor->set_attribute( 'class', $this->toggle_state_class( (string) $processor->get_attribute( 'class' ), 'is-active', $is_active ) ); + } + + if ( 'A' === $processor->get_tag() && 'tab' === $processor->get_attribute( 'role' ) ) { + $processor->set_attribute( 'aria-selected', $is_active ? 'true' : 'false' ); + if ( $is_active ) { + $processor->remove_attribute( 'tabindex' ); + } else { + $processor->set_attribute( 'tabindex', '-1' ); + } + } + } + + return $processor->get_updated_html(); + } + + /** + * @param string $html Markup. + * @param array $attrs Block attributes. + * @return string + */ + private function transform_tabs_panel_item_markup( string $html, array $attrs ): string { + $index = (int) ( $attrs['index'] ?? 0 ); + $is_active = 0 === $index; + + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { + return $html; + } + + $processor = new \WP_HTML_Tag_Processor( $html ); + while ( $processor->next_tag() ) { + if ( 'DIV' !== $processor->get_tag() || 'tabpanel' !== $processor->get_attribute( 'role' ) ) { + continue; + } + + $class = (string) $processor->get_attribute( 'class' ); + $class = $this->toggle_state_class( $class, 'is-active', $is_active ); + $class = $this->toggle_state_class( $class, 'is-hidden', ! $is_active ); + $processor->set_attribute( 'class', $class ); + } + + return $processor->get_updated_html(); + } + + /** + * @param string $class Existing class string. + * @param string $state_class State class to add or remove. + * @param bool $enable Whether the state class should be present. + * @return string + */ + private function toggle_state_class( string $class, string $state_class, bool $enable ): string { + $parts = array_filter( preg_split( '/\s+/', trim( $class ) ) ?: [] ); + $parts = array_values( array_diff( $parts, [ $state_class ] ) ); + + if ( $enable ) { + $parts[] = $state_class; + } + + return implode( ' ', $parts ); + } +} diff --git a/package-lock.json b/package-lock.json index 01b2def..22d2772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockparty-tabs", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockparty-tabs", - "version": "2.0.1", + "version": "2.1.0", "license": "GPL-2.0-or-later", "dependencies": { "classnames": "^2.5.1" diff --git a/package.json b/package.json index e147123..2028068 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockparty-tabs", - "version": "2.0.1", + "version": "2.1.0", "description": "Accessible tabs block for WordPress", "author": "Be API Technical team", "license": "GPL-2.0-or-later", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6c00134..d9ec446 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,6 +18,7 @@ blockparty-tabs.php + includes diff --git a/readme.txt b/readme.txt index 71cfd19..3b5db6d 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: block, tabs, accessibility Requires at least: 6.4 Requires PHP: 8.1 Tested up to: 7.1 -Stable tag: 2.0.1 +Stable tag: 2.1.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -33,6 +33,13 @@ directory take precedence. For example, `/assets/screenshot-1.png` would win ove == Changelog == += 2.1.0 = + +* Add WP-CLI command `wp blockparty-tabs migrate-from-tabs-block` to migrate `beapi/tabs*` content to `blockparty/tabs*` +* Add `MIGRATION.md` for upgrading from `beapi/tabs-block` +* Add PHPUnit coverage for the tabs block migrator +* Skip block registration when compiled `build/` assets are missing + = 2.0.1 = * Fix full-width (`alignfull`) tabs so panel padding is reset correctly diff --git a/src/blockparty-tabs-nav-item/block.json b/src/blockparty-tabs-nav-item/block.json index 6598d60..a274dde 100644 --- a/src/blockparty-tabs-nav-item/block.json +++ b/src/blockparty-tabs-nav-item/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-nav-item", - "version": "2.0.1", + "version": "2.1.0", "title": "Tab", "category": "widgets", "description": "A single tab button used to switch to its matching content panel.", diff --git a/src/blockparty-tabs-nav/block.json b/src/blockparty-tabs-nav/block.json index 774b8b8..31f36f3 100644 --- a/src/blockparty-tabs-nav/block.json +++ b/src/blockparty-tabs-nav/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-nav", - "version": "2.0.1", + "version": "2.1.0", "title": "Tabs list", "category": "widgets", "description": "Display the tab list for switching between content panels.", diff --git a/src/blockparty-tabs-panel-item/block.json b/src/blockparty-tabs-panel-item/block.json index c915e3d..be8a31c 100644 --- a/src/blockparty-tabs-panel-item/block.json +++ b/src/blockparty-tabs-panel-item/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-panel-item", - "version": "2.0.1", + "version": "2.1.0", "title": "Panel", "category": "widgets", "icon": "layout", diff --git a/src/blockparty-tabs-panels/block.json b/src/blockparty-tabs-panels/block.json index 9437707..b01a989 100644 --- a/src/blockparty-tabs-panels/block.json +++ b/src/blockparty-tabs-panels/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-panels", - "version": "2.0.1", + "version": "2.1.0", "title": "Panels", "category": "widgets", "description": "Group the content panels of a tabbed interface.", diff --git a/src/blockparty-tabs/block.json b/src/blockparty-tabs/block.json index d30c5f1..4bd2ebe 100644 --- a/src/blockparty-tabs/block.json +++ b/src/blockparty-tabs/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs", - "version": "2.0.1", + "version": "2.1.0", "title": "Tabs", "category": "widgets", "description": "Organize content into tabs so visitors can browse sections without scrolling endlessly.", diff --git a/tests/php/TabsBlockMigratorTest.php b/tests/php/TabsBlockMigratorTest.php new file mode 100644 index 0000000..e37f68e --- /dev/null +++ b/tests/php/TabsBlockMigratorTest.php @@ -0,0 +1,123 @@ + +
+ + + + +
+
+

Panel one

+
+ + + +
+

Panel two

+
+
+
+ +CONTENT; + + $migrator = new TabsBlockMigrator(); + $result = $migrator->migrate_content( $content ); + + $this->assertNotNull( $result ); + $this->assertStringContainsString( 'blockparty/tabs', $result ); + $this->assertStringNotContainsString( 'beapi/tabs', $result ); + $this->assertStringContainsString( 'wp-block-blockparty-tabs', $result ); + $this->assertStringNotContainsString( 'wp-block-beapi-tabs', $result ); + $this->assertStringNotContainsString( '"title":"Our sections"', $result ); + $this->assertStringContainsString( '"ariaLabel":"Our sections"', $result ); + $this->assertStringContainsString( 'role="tablist"', $result ); + $this->assertStringNotContainsString( 'wp-block-blockparty-tabs has-align-top-left" aria-label', $result ); + $this->assertStringContainsString( 'aria-selected="true"', $result ); + $this->assertStringContainsString( 'aria-selected="false"', $result ); + $this->assertStringContainsString( 'tabindex="-1"', $result ); + $this->assertStringContainsString( 'wp-block-blockparty-tabs-panel-item is-active', $result ); + $this->assertStringContainsString( 'wp-block-blockparty-tabs-panel-item is-hidden', $result ); + $this->assertStringContainsString( 'wp-block-blockparty-tabs-nav-item is-active', $result ); + $this->assertGreaterThan( 0, $migrator->migrated ); + } + + /** + * @return void + */ + public function test_migrates_nested_tabs_inside_panel(): void { + $content = <<<'CONTENT' + +
+ + + + +
+
+
+ + + + +
+
+

Nested

+
+
+
+
+
+
+ +CONTENT; + + $migrator = new TabsBlockMigrator(); + $result = $migrator->migrate_content( $content ); + + $this->assertNotNull( $result ); + $this->assertSame( 10, $migrator->migrated ); + $this->assertStringNotContainsString( 'beapi/tabs', $result ); + $this->assertStringContainsString( 'blockparty/tabs', $result ); + } + + /** + * @return void + */ + public function test_returns_null_when_no_legacy_tabs(): void { + $migrator = new TabsBlockMigrator(); + $content = '

Hello

'; + + $this->assertNull( $migrator->migrate_content( $content ) ); + } +} From 370a0e0bc5e6160a76d1e24bc82b784101069fdb Mon Sep 17 00:00:00 2001 From: Nicolas Pays Date: Thu, 8 Oct 2026 11:19:29 +0200 Subject: [PATCH 2/4] fix vendor autoload tests --- blockparty-tabs.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blockparty-tabs.php b/blockparty-tabs.php index 72af5fa..1cd8c03 100644 --- a/blockparty-tabs.php +++ b/blockparty-tabs.php @@ -16,6 +16,10 @@ use Blockparty\Tabs\Cli\MigrateFromTabsBlockCommand; +if ( is_readable( __DIR__ . '/vendor/autoload.php' ) ) { + include_once __DIR__ . '/vendor/autoload.php'; +} + define( 'BLOCKPARTY_TABS_VERSION', '2.0.1' ); define( 'BLOCKPARTY_TABS_URL', plugin_dir_url( __FILE__ ) ); define( 'BLOCKPARTY_TABS_DIR', plugin_dir_path( __FILE__ ) ); @@ -177,6 +181,6 @@ function register_react_jsx_runtime( $scripts ): void { add_action( 'wp_default_scripts', __NAMESPACE__ . '\\register_react_jsx_runtime' ); -if ( defined( 'WP_CLI' ) && WP_CLI ) { +if ( defined( 'WP_CLI' ) && WP_CLI && class_exists( MigrateFromTabsBlockCommand::class ) ) { \WP_CLI::add_command( 'blockparty-tabs migrate-from-tabs-block', MigrateFromTabsBlockCommand::class ); } From 9caf7156ebaf0e09d98d864e071ca7c836fd9c71 Mon Sep 17 00:00:00 2001 From: Nicolas Pays Date: Fri, 9 Oct 2026 09:21:36 +0200 Subject: [PATCH 3/4] rollback version --- .plugin-data | 2 +- .wordpress-org/blueprints/blueprint.json | 2 +- CHANGELOG.md | 7 ------- composer.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- readme.txt | 9 +-------- src/blockparty-tabs-nav-item/block.json | 2 +- src/blockparty-tabs-nav/block.json | 2 +- src/blockparty-tabs-panel-item/block.json | 2 +- src/blockparty-tabs-panels/block.json | 2 +- src/blockparty-tabs/block.json | 2 +- 12 files changed, 12 insertions(+), 26 deletions(-) diff --git a/.plugin-data b/.plugin-data index 41ef40d..3f8d940 100644 --- a/.plugin-data +++ b/.plugin-data @@ -1,4 +1,4 @@ { - "version": "2.1.0", + "version": "2.0.1", "slug": "blockparty-tabs" } diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json index 602eacd..c0ad62b 100644 --- a/.wordpress-org/blueprints/blueprint.json +++ b/.wordpress-org/blueprints/blueprint.json @@ -11,7 +11,7 @@ "pluginData": { "resource": "git:directory", "url": "https://github.com/BeAPI/blockparty-tabs", - "ref": "2.1.0", + "ref": "2.0.1", "refType": "tag" }, "options": { diff --git a/CHANGELOG.md b/CHANGELOG.md index b89a22a..51172a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 2.1.0 - 2026-10-08 - -- Add WP-CLI command `wp blockparty-tabs migrate-from-tabs-block` to migrate `beapi/tabs*` content to `blockparty/tabs*` (`--dry-run`, `--post-type`, `--posts-per-page`) -- Add `MIGRATION.md` for upgrading from `beapi/tabs-block` -- Add PHPUnit coverage for the tabs block migrator -- Skip block registration when compiled `build/` assets are missing - ## 2.0.1 - 2026-09-29 - Fix full-width (`alignfull`) tabs so panel padding is reset correctly diff --git a/composer.json b/composer.json index e23eb56..9204afd 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "beapi/blockparty-tabs", - "version": "2.1.0", + "version": "2.0.1", "description": "Accessible Tabs block for WordPress gutenberg.", "type": "wordpress-plugin", "keywords": ["wordpress", "plugin", "tabs", "gutenberg"], diff --git a/package-lock.json b/package-lock.json index 22d2772..01b2def 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blockparty-tabs", - "version": "2.1.0", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blockparty-tabs", - "version": "2.1.0", + "version": "2.0.1", "license": "GPL-2.0-or-later", "dependencies": { "classnames": "^2.5.1" diff --git a/package.json b/package.json index 2028068..e147123 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockparty-tabs", - "version": "2.1.0", + "version": "2.0.1", "description": "Accessible tabs block for WordPress", "author": "Be API Technical team", "license": "GPL-2.0-or-later", diff --git a/readme.txt b/readme.txt index 3b5db6d..71cfd19 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: block, tabs, accessibility Requires at least: 6.4 Requires PHP: 8.1 Tested up to: 7.1 -Stable tag: 2.1.0 +Stable tag: 2.0.1 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -33,13 +33,6 @@ directory take precedence. For example, `/assets/screenshot-1.png` would win ove == Changelog == -= 2.1.0 = - -* Add WP-CLI command `wp blockparty-tabs migrate-from-tabs-block` to migrate `beapi/tabs*` content to `blockparty/tabs*` -* Add `MIGRATION.md` for upgrading from `beapi/tabs-block` -* Add PHPUnit coverage for the tabs block migrator -* Skip block registration when compiled `build/` assets are missing - = 2.0.1 = * Fix full-width (`alignfull`) tabs so panel padding is reset correctly diff --git a/src/blockparty-tabs-nav-item/block.json b/src/blockparty-tabs-nav-item/block.json index a274dde..6598d60 100644 --- a/src/blockparty-tabs-nav-item/block.json +++ b/src/blockparty-tabs-nav-item/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-nav-item", - "version": "2.1.0", + "version": "2.0.1", "title": "Tab", "category": "widgets", "description": "A single tab button used to switch to its matching content panel.", diff --git a/src/blockparty-tabs-nav/block.json b/src/blockparty-tabs-nav/block.json index 31f36f3..774b8b8 100644 --- a/src/blockparty-tabs-nav/block.json +++ b/src/blockparty-tabs-nav/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-nav", - "version": "2.1.0", + "version": "2.0.1", "title": "Tabs list", "category": "widgets", "description": "Display the tab list for switching between content panels.", diff --git a/src/blockparty-tabs-panel-item/block.json b/src/blockparty-tabs-panel-item/block.json index be8a31c..c915e3d 100644 --- a/src/blockparty-tabs-panel-item/block.json +++ b/src/blockparty-tabs-panel-item/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-panel-item", - "version": "2.1.0", + "version": "2.0.1", "title": "Panel", "category": "widgets", "icon": "layout", diff --git a/src/blockparty-tabs-panels/block.json b/src/blockparty-tabs-panels/block.json index b01a989..9437707 100644 --- a/src/blockparty-tabs-panels/block.json +++ b/src/blockparty-tabs-panels/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs-panels", - "version": "2.1.0", + "version": "2.0.1", "title": "Panels", "category": "widgets", "description": "Group the content panels of a tabbed interface.", diff --git a/src/blockparty-tabs/block.json b/src/blockparty-tabs/block.json index 4bd2ebe..d30c5f1 100644 --- a/src/blockparty-tabs/block.json +++ b/src/blockparty-tabs/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "blockparty/tabs", - "version": "2.1.0", + "version": "2.0.1", "title": "Tabs", "category": "widgets", "description": "Organize content into tabs so visitors can browse sections without scrolling endlessly.", From 2988529b183ebfbc5d8230a8ed2c9b7affdd7077 Mon Sep 17 00:00:00 2001 From: Nicolas Pays Date: Fri, 9 Oct 2026 09:23:18 +0200 Subject: [PATCH 4/4] rollback version --- composer.json | 114 ++++++++++++++++++++++++++------------------------ 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/composer.json b/composer.json index 9204afd..db4d0eb 100644 --- a/composer.json +++ b/composer.json @@ -1,56 +1,60 @@ { - "name": "beapi/blockparty-tabs", - "version": "2.0.1", - "description": "Accessible Tabs block for WordPress gutenberg.", - "type": "wordpress-plugin", - "keywords": ["wordpress", "plugin", "tabs", "gutenberg"], - "license": "GPL-2.0-or-later", - "authors": [ - { - "name": "BeAPI", - "email": "technique@beapi.fr" - } - ], - "config": { - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true, - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "composer/installers": true, - "phpro/grumphp-shim": true - }, - "platform": { - "php": "8.1.99" - } - }, - "autoload": { - "psr-4": { - "Blockparty\\Tabs\\": "includes/" - } - }, - "require": { - "php": "^8.1", - "composer/installers": "^1.0 || ^2.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "overtrue/phplint": "^9.1", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcompatibility/phpcompatibility-wp": "^2.1", - "phpro/grumphp-shim": "^1.5", - "phpunit/phpunit": "9.6.37", - "roave/security-advisories": "dev-latest", - "wp-coding-standards/wpcs": "^3.0", - "wp-phpunit/wp-phpunit": "7.1.1", - "yoast/phpunit-polyfills": "4.0.0" - }, - "scripts": { - "cs": "./vendor/bin/phpcs", - "cb": "./vendor/bin/phpcbf" - }, - "scripts-descriptions": { - "cs": "Run PHP CodeSniffer on codebase using custom ruleset.", - "cb": "Run PHP Code Beautifier and Fixer on codebase using custom ruleset." - } -} + "name": "beapi/blockparty-tabs", + "description": "Accessible Tabs block for WordPress gutenberg.", + "type": "wordpress-plugin", + "keywords": [ + "wordpress", + "plugin", + "tabs", + "gutenberg" + ], + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "BeAPI", + "email": "technique@beapi.fr" + } + ], + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "composer/installers": true, + "phpro/grumphp-shim": true + }, + "platform": { + "php": "8.1.99" + } + }, + "autoload": { + "psr-4": { + "Blockparty\\Tabs\\": "includes/" + } + }, + "require": { + "php": "^8.1", + "composer/installers": "^1.0 || ^2.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "overtrue/phplint": "^9.1", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpro/grumphp-shim": "^1.5", + "phpunit/phpunit": "9.6.37", + "roave/security-advisories": "dev-latest", + "wp-coding-standards/wpcs": "^3.0", + "wp-phpunit/wp-phpunit": "7.1.1", + "yoast/phpunit-polyfills": "4.0.0" + }, + "scripts": { + "cs": "./vendor/bin/phpcs", + "cb": "./vendor/bin/phpcbf" + }, + "scripts-descriptions": { + "cs": "Run PHP CodeSniffer on codebase using custom ruleset.", + "cb": "Run PHP Code Beautifier and Fixer on codebase using custom ruleset." + } +} \ No newline at end of file