diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5e363..6149ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.2.0 +* Fix: Resolve `Image` block import issues. +* Feat: Add custom hooks `cbtj_import_block`, `cbtj_export_block`, `cbtj_blocks`. +* Docs: Update README docs. +* Tested up to WP 6.8. + ## 1.1.0 * Feat: Add REST namespace filter `cbtj_rest_namespace`. * Refactor: Use classes for PHP codebase. diff --git a/README.md b/README.md index 37f5d79..5c96263 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,79 @@ https://github.com/user-attachments/assets/9dedf30f-9df0-4307-b634-cecef930a6e5 ### Hooks +#### `cbtj_blocks` + +This custom hook (filter) provides the ability to customise the block classes: + +```php +add_filter( 'cbtj_blocks', [ $this, 'custom_blocks' ], 10 ); + +public function custom_blocks( $blocks ): array { + $blocks[] = \YourNameSpace\YourCustomBlock::class; + + return $block; +} +``` + +**Parameters** + +- blocks _`{Block[]}`_ By default, this is an array consisting of block classes. +
+ +#### `cbtj_import_block` + +This custom hook (filter) provides the ability to customise any block array during import: + +```php +add_filter( 'cbtj_import_block', [ $this, 'custom_import_block' ], 10 ); + +public function custom_import_block( $block ): array { + if ( 'core/image' !== $block['name'] ) { + return $block; + } + + // Get block attributes. + $block['attributes'] = json_decode( $block['attributes'], true ); + + // Set Caption using Post meta. + $block['attributes']['caption'] = get_post_meta( get_the_ID(), 'featured_image_caption', true ); + + // Encode attributes finally. + $block['attributes'] = wp_json_encode( $block['attributes'] ); + + return $block; +} +``` + +**Parameters** + +- block _`{mixed[]}`_ By default, this would be a block array containing `name`, `originalContent`, `attributes` & `innerBlocks` key/value pairs. +
+ +#### `cbtj_export_block` + +This custom hook (filter) provides the ability to customise any block array during export: + +```php +add_filter( 'cbtj_export_block', [ $this, 'custom_export_block' ], 10 ); + +public function custom_export_block( $block ): array { + if ( 'core/image' !== $block['name'] ) { + return $block; + } + + // Set Caption using Post meta. + $block['attributes']['caption'] = get_post_meta( get_the_ID(), 'featured_image_caption', true ); + + return $block; +} +``` + +**Parameters** + +- block _`{mixed[]}`_ By default, this would be a block array containing `name`, `content`, `filtered`, `attributes` & `innerBlocks` key/value pairs. +
+ #### `cbtj_rest_export` This custom hook (filter) provides the ability to customise the REST response obtained: diff --git a/inc/Abstracts/Block.php b/inc/Abstracts/Block.php new file mode 100644 index 0000000..d1cfc51 --- /dev/null +++ b/inc/Abstracts/Block.php @@ -0,0 +1,37 @@ + $block['blockName'] ?? '', 'content' => $block['innerHTML'] ?? '', 'filtered' => wp_strip_all_tags( $block['innerHTML'] ?? '' ), 'attributes' => $block['attrs'] ?? [], 'innerBlocks' => $children, ]; + + /** + * Filter Export Block. + * + * @since 1.2.0 + * + * @param mixed[] $export_block Export Block. + * @return mixed[] + */ + return apply_filters( 'cbtj_export_block', $export_block ); } } diff --git a/inc/Routes/Import.php b/inc/Routes/Import.php index ea8f156..0b0d609 100644 --- a/inc/Routes/Import.php +++ b/inc/Routes/Import.php @@ -76,16 +76,25 @@ public function rest_callback( $request ) { } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents - $json = file_get_contents( $json_file ); - $import = $this->get_blocks_import( json_decode( $json, true ), $post_id ); + $json = json_decode( file_get_contents( $json_file ), true ); + $blocks = $this->get_blocks_import( $json['content'] ?? [], $post_id ); + + // Make sure to weed out empty blocks. + $content = array_filter( $blocks, fn( $block ) => ! empty( $block ) ); + + // Add title. + $import = [ + 'title' => $json['title'] ?? '', + 'content' => $content + ]; /** - * Filter JSON Import. + * Filter Import. * * @since 1.0.1 * - * @param mixed[] $response Import Blocks. - * @param integer $post_id Post ID. + * @param mixed[] $import Import data. + * @param integer $post_id Post ID. * * @return mixed[] */ @@ -102,18 +111,13 @@ public function rest_callback( $request ) { * * @param 1.0.1 * - * @param array $json JSON Array of Blocks. + * @param array $content JSON content. * @param integer $post_id Post ID. * * @return mixed[] */ - public function get_blocks_import( $json, $post_id ): array { - $import_blocks = array_map( - [ $this, 'get_import' ], - $json['content'] ?? [] - ); - - return $import_blocks; + public function get_blocks_import( $content, $post_id ): array { + return array_map( [ $this, 'get_import' ], $content ); } /** @@ -128,6 +132,11 @@ public function get_blocks_import( $json, $post_id ): array { * @return mixed[] */ public function get_import( $block ): array { + // Bail out, if block has no name. + if ( '' === ( $block['name'] ?? '' ) ) { + return []; + } + $children = []; if ( ! empty( $block['innerBlocks'] ) ) { @@ -138,10 +147,21 @@ public function get_import( $block ): array { $block['attributes']['content'] = $block['filtered'] ?? ''; - return [ - 'name' => $block['name'] ?? '', - 'attributes' => wp_json_encode( $block['attributes'] ?? [] ), - 'innerBlocks' => $children ?? [], + $import_block = [ + 'name' => $block['name'] ?? '', + 'originalContent' => $block['content'] ?? '', + 'attributes' => wp_json_encode( $block['attributes'] ?? [] ), + 'innerBlocks' => $children ?? [], ]; + + /** + * Filter Import Block. + * + * @since 1.2.0 + * + * @param mixed[] $import_block Import Block. + * @return mixed[] + */ + return apply_filters( 'cbtj_import_block', $import_block ); } } diff --git a/inc/Services/Blocks.php b/inc/Services/Blocks.php new file mode 100644 index 0000000..9b4d891 --- /dev/null +++ b/inc/Services/Blocks.php @@ -0,0 +1,55 @@ +init(); + } + } +} diff --git a/readme.txt b/readme.txt index 902806d..e13089d 100644 --- a/readme.txt +++ b/readme.txt @@ -64,6 +64,12 @@ Want to add your personal touch? All of our documentation can be found [here](ht == Changelog == += 1.2.0 = +* Fix: Resolve `Image` block import issues. +* Feat: Add custom hooks `cbtj_import_block`, `cbtj_export_block`, `cbtj_blocks`. +* Docs: Update README docs. +* Tested up to WP 6.8. + = 1.1.0 = * Feat: Add REST namespace filter `cbtj_rest_namespace`. * Refactor: Use classes for PHP codebase. diff --git a/src/components/ImportJSON.tsx b/src/components/ImportJSON.tsx index a689748..55c7d4b 100644 --- a/src/components/ImportJSON.tsx +++ b/src/components/ImportJSON.tsx @@ -2,6 +2,9 @@ import { __ } from '@wordpress/i18n'; import { dispatch } from '@wordpress/data'; import { Button } from '@wordpress/components'; import { createBlock } from '@wordpress/blocks'; +import { store as editorStore } from '@wordpress/editor'; +import { store as noticeStore } from '@wordpress/notices'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import { getModalParams, getImport } from '../utils'; @@ -40,26 +43,38 @@ const ImportJSON = (): JSX.Element => { * * @since 1.0.1 * - * @param {Object} wpMediaModal - * + * @param {any} wpMediaModal * @return {Promise} */ - const handleImport = async ( wpMediaModal ) => { + const handleImport = async ( wpMediaModal: any ): Promise< void > => { const attachment = wpMediaModal .state() .get( 'selection' ) .first() .toJSON(); - const jsonImport = ( await getImport( attachment ) ) as any[]; - jsonImport.forEach( ( { name, attributes, innerBlocks } ) => { - attributes = JSON.parse( attributes ); - ( - dispatch( 'core/block-editor' ) as { insertBlocks: any } - ).insertBlocks( - createBlock( name, { ...attributes }, innerBlocks ) - ); - } ); + try { + // Get data. + const { title, content } = await getImport( attachment ); + + // Add title. + dispatch( editorStore ).editPost( { title, status: 'publish' } ); + + // Add content. + content.forEach( ( { name, attributes, innerBlocks } ) => { + attributes = JSON.parse( attributes ); + ( + dispatch( blockEditorStore ) as { insertBlocks: any } + ).insertBlocks( + createBlock( name, { ...attributes }, innerBlocks ) + ); + } ); + + // Save Post. + await dispatch( editorStore ).savePost(); + } catch ( e ) { + dispatch( noticeStore ).createWarningNotice( e.message ); + } }; return ( diff --git a/src/utils.tsx b/src/utils.tsx index e635ba0..9a2ba0c 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -1,6 +1,7 @@ import { __ } from '@wordpress/i18n'; import { select } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; +import { store as editorStore } from '@wordpress/editor'; /** * Get Blocks. @@ -13,10 +14,10 @@ import apiFetch from '@wordpress/api-fetch'; * * @return {Promise} Blocks. */ -export const getBlocks = (): Promise< any[] > => { - const postID = select( 'core/editor' ).getCurrentPostId(); +export const getBlocks = async (): Promise< any[] > => { + const postID = select( editorStore ).getCurrentPostId(); - return apiFetch( { + return await apiFetch( { path: `/cbtj/v1/${ postID }`, } ); }; @@ -27,13 +28,13 @@ export const getBlocks = (): Promise< any[] > => { * This function reaches out to the import endpoint * and gets the list of JSON blocks. * - * @param attachment * @since 1.0.1 * - * @return {Promise} Import. + * @param {any} attachment Attachment object. + * @return {Promise} Import. */ -export const getImport = ( attachment ): Promise< any[] > => { - return apiFetch( { +export const getImport = async ( attachment: any ): Promise< any > => { + return await apiFetch( { path: '/cbtj/v1/import', method: 'POST', data: { @@ -51,9 +52,9 @@ export const getImport = ( attachment ): Promise< any[] > => { * * @since 1.0.1 * - * @return {Object} Modal Params. + * @return {any} Modal Params. */ -export const getModalParams = (): object => { +export const getModalParams = (): any => { return { title: __( 'Select JSON File', 'convert-blocks-to-json' ), button: { diff --git a/tests/Args.test.tsx b/tests/Args.test.tsx index 6ecce25..30ea171 100644 --- a/tests/Args.test.tsx +++ b/tests/Args.test.tsx @@ -2,6 +2,10 @@ import '@testing-library/jest-dom'; import { getBlocks, getModalParams, getImport } from '../src/utils'; +jest.mock( '@wordpress/editor', () => ( { + store: 'core/editor', +} ) ); + jest.mock( '@wordpress/data', () => ( { select: jest.fn( ( arg ) => { if ( 'core/editor' === arg ) { @@ -14,7 +18,7 @@ jest.mock( '@wordpress/data', () => ( { } ) ); jest.mock( '@wordpress/api-fetch', () => { - function apiFetchMock( options ) { + function apiFetchMock( options: any ) { const { path, method } = options; if ( '/cbtj/v1/7' === path ) { diff --git a/tests/ExportJSON.test.tsx b/tests/ExportJSON.test.tsx index c604061..6d21ce7 100644 --- a/tests/ExportJSON.test.tsx +++ b/tests/ExportJSON.test.tsx @@ -1,9 +1,12 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import '@testing-library/jest-dom'; import ExportJSON from '../src/components/ExportJSON'; +jest.mock( '@wordpress/editor', () => ( { + store: 'core/editor', +} ) ); + jest.mock( '@wordpress/data', () => ( { select: jest.fn( ( storeName ) => { if ( storeName === 'core/editor' ) { diff --git a/tests/ViewJSON.test.tsx b/tests/ViewJSON.test.tsx index 2d48d22..5e3931d 100644 --- a/tests/ViewJSON.test.tsx +++ b/tests/ViewJSON.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import '@testing-library/jest-dom'; import ViewJSON from '../src/components/ViewJSON'; @@ -26,7 +26,9 @@ jest.mock( '@wordpress/components', () => ( { describe( 'ViewJSON', () => { beforeAll( () => { - global.cbtj = { baseUrl: 'https://example.com' }; + ( global as unknown as { cbtj: any } ).cbtj = { + baseUrl: 'https://example.com', + }; } ); it( 'renders the component with correct text and link', () => {