From c5fa5a14eb5dd432016679311e905e611392d52a Mon Sep 17 00:00:00 2001 From: Aurorum <43215253+Aurorum@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:14:18 +0000 Subject: [PATCH] Gutenberg: Add Goodreads Block (#33395) * Add slug * First stab at Goodreads block * Load block front-end * Handle author profiles * Endpoint for getting user ID * Remove unneeded parameter * Add tests * Address feedback * Update class-wpcom-rest-api-v2-endpoint-goodreads.php * Update goodreads.php * Rewrite to functional component * Address feedback * Update edit.js * Fix block registration * Fix block registration * Refactor block * Remove legacy widget * Indentation fix * Fix tests * Manually update files * Manually update file * Fix bookNumber not saving --------- Co-authored-by: Jeremy Herve --- ...s-wpcom-rest-api-v2-endpoint-goodreads.php | 78 ++++++++ .../jetpack/changelog/add-goodreads-block | 4 + .../extensions/blocks/goodreads/block.json | 89 +++++++++ .../extensions/blocks/goodreads/controls.js | 131 +++++++++++++ .../extensions/blocks/goodreads/edit.js | 175 ++++++++++++++++++ .../extensions/blocks/goodreads/editor.js | 9 + .../extensions/blocks/goodreads/goodreads.php | 50 +++++ .../hooks/use-fetch-goodreads-data.js | 76 ++++++++ .../extensions/blocks/goodreads/save.js | 10 + .../extensions/blocks/goodreads/style.scss | 91 +++++++++ .../blocks/goodreads/test/controls.js | 163 ++++++++++++++++ .../extensions/blocks/goodreads/test/edit.js | 107 +++++++++++ .../test/fixtures/jetpack__goodreads.html | 3 + .../test/fixtures/jetpack__goodreads.json | 27 +++ .../fixtures/jetpack__goodreads.parsed.json | 14 ++ .../jetpack__goodreads.serialized.html | 3 + .../extensions/blocks/goodreads/test/utils.js | 28 +++ .../blocks/goodreads/test/validate.js | 9 + .../extensions/blocks/goodreads/utils.js | 87 +++++++++ .../extensions/blocks/goodreads/view.js | 1 + .../plugins/jetpack/extensions/index.json | 1 + .../jetpack/modules/widgets/goodreads.php | 12 ++ 22 files changed, 1168 insertions(+) create mode 100644 projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-goodreads.php create mode 100644 projects/plugins/jetpack/changelog/add-goodreads-block create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/block.json create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/controls.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/edit.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/editor.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/goodreads.php create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/hooks/use-fetch-goodreads-data.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/save.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/style.scss create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/test/controls.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/test/edit.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.html create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.json create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.parsed.json create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.serialized.html create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/test/utils.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/test/validate.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/utils.js create mode 100644 projects/plugins/jetpack/extensions/blocks/goodreads/view.js diff --git a/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-goodreads.php b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-goodreads.php new file mode 100644 index 0000000000000..59ccb3863e2ba --- /dev/null +++ b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-goodreads.php @@ -0,0 +1,78 @@ + WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_goodreads_user_id' ), + 'permission_callback' => function () { + return current_user_can( 'edit_posts' ); + }, + 'args' => array( + 'id' => array( + 'description' => __( 'Goodreads user ID', 'jetpack' ), + 'type' => 'integer', + 'required' => true, + 'minimum' => 1, + 'validate_callback' => function ( $param ) { + return is_numeric( $param ) && (int) $param > 0; + }, + ), + ), + ), + ) + ); + } + + /** + * Get the user ID from the author ID. + * + * @param \WP_REST_Request $request request object. + * + * @return int Goodreads user ID (or 404 if not found). + */ + public function get_goodreads_user_id( $request ) { + $profile_id = $request->get_param( 'id' ); + $url = 'https://www.goodreads.com/author/show/' . $profile_id; + $response = wp_remote_get( esc_url_raw( $url ) ); + $not_found = new WP_Error( 'not_found', 'Goodreads user not found.', array( 'status' => 404 ) ); + + if ( is_wp_error( $response ) ) { + return $not_found; + } + + $body = wp_remote_retrieve_body( $response ); + $pattern = '/goodreads\.com\/user\/updates_rss\/(\d+)/'; + + if ( preg_match( $pattern, $body, $matches ) ) { + $user_id = intval( $matches[1] ); + return $user_id; + } + + return $not_found; + } +} + +wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Goodreads' ); diff --git a/projects/plugins/jetpack/changelog/add-goodreads-block b/projects/plugins/jetpack/changelog/add-goodreads-block new file mode 100644 index 0000000000000..449aa799415bb --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-goodreads-block @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Comment: Add Goodreads embed block in Gutenberg. diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/block.json b/projects/plugins/jetpack/extensions/blocks/goodreads/block.json new file mode 100644 index 0000000000000..f5eb4ee8bbe0b --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/block.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 1, + "name": "jetpack/goodreads", + "title": "Goodreads", + "description": "Features books from the shelves of your Goodreads account.", + "keywords": [ "book", "read", "author" ], + "version": "1.0", + "textdomain": "jetpack", + "category": "embed", + "icon": "", + "supports": { + "html": false, + "align": true + }, + "attributes": { + "bookNumber": { + "type": "string", + "default": "5" + }, + "class": { + "type": "string" + }, + "customTitle": { + "type": "string", + "default": "My Bookshelf" + }, + "goodreadsId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "link": { + "type": "string" + }, + "orderOption": { + "type": "string", + "default": "a" + }, + "shelfOption": { + "type": "string", + "default": "read" + }, + "showAuthor": { + "type": "boolean", + "default": true + }, + "showCover": { + "type": "boolean", + "default": true + }, + "showRating": { + "type": "boolean", + "default": true + }, + "showReview": { + "type": "boolean", + "default": false + }, + "showTags": { + "type": "boolean", + "default": false + }, + "showTitle": { + "type": "boolean", + "default": true + }, + "sortOption": { + "type": "string", + "default": "date_added" + }, + "style": { + "type": "string", + "default": "default" + }, + "userInput": { + "type": "string" + }, + "widgetId": { + "type": "number" + } + }, + "example": { + "attributes": { + "goodreadsId": 1176283 + } + } +} diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/controls.js b/projects/plugins/jetpack/extensions/blocks/goodreads/controls.js new file mode 100644 index 0000000000000..46a4a426a8ea8 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/controls.js @@ -0,0 +1,131 @@ +import { + PanelBody, + SelectControl, + TextControl, + ToggleControl, + ToolbarButton, + ToolbarGroup, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { GOODREADS_SHELF_OPTIONS, GOODREADS_ORDER_OPTIONS, GOODREADS_SORT_OPTIONS } from './utils'; + +const renderGoodreadsDisplaySettings = ( { attributes, setAttributes } ) => { + const { showCover, showAuthor, showTitle, showRating, showReview, showTags } = attributes; + + return ( + + setAttributes( { showCover: ! showCover } ) } + /> + + setAttributes( { showAuthor: ! showAuthor } ) } + /> + + setAttributes( { showTitle: ! showTitle } ) } + /> + + setAttributes( { showRating: ! showRating } ) } + /> + + setAttributes( { showReview: ! showReview } ) } + /> + + setAttributes( { showTags: ! showTags } ) } + /> + + ); +}; + +export function GoodreadsInspectorControls( { attributes, setAttributes } ) { + const { style, shelfOption, bookNumber, orderOption, customTitle, sortOption } = attributes; + + return ( + <> + + setAttributes( { shelfOption: value } ) } + options={ GOODREADS_SHELF_OPTIONS } + /> + + setAttributes( { customTitle: value } ) } + /> + + setAttributes( { sortOption: value } ) } + options={ GOODREADS_SORT_OPTIONS } + /> + + setAttributes( { orderOption: value } ) } + options={ GOODREADS_ORDER_OPTIONS } + /> + + setAttributes( { bookNumber: value } ) } + /> + + { style === 'default' && renderGoodreadsDisplaySettings( { attributes, setAttributes } ) } + + ); +} + +export function GoodreadsBlockControls( { attributes, setAttributes, setDisplayPreview } ) { + const { style } = attributes; + const layoutControls = [ + { + icon: 'list-view', + title: __( 'Default view', 'jetpack' ), + onClick: () => setAttributes( { style: 'default' } ), + isActive: style === 'default', + }, + { + icon: 'grid-view', + title: __( 'Grid view', 'jetpack' ), + onClick: () => setAttributes( { style: 'grid' } ), + isActive: style === 'grid', + }, + ]; + + return ( + <> + setDisplayPreview( false ) } + /> + + + ); +} diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/edit.js b/projects/plugins/jetpack/extensions/blocks/goodreads/edit.js new file mode 100644 index 0000000000000..4d902ec2a4c46 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/edit.js @@ -0,0 +1,175 @@ +import { getBlockIconComponent } from '@automattic/jetpack-shared-extension-utils'; +import { BlockControls, InspectorControls } from '@wordpress/block-editor'; +import { Placeholder, SandBox, Button, Spinner, withNotices } from '@wordpress/components'; +import { useState, useEffect, useRef } from '@wordpress/element'; +import { __, _x } from '@wordpress/i18n'; +import metadata from './block.json'; +import { GoodreadsBlockControls, GoodreadsInspectorControls } from './controls'; +import useFetchGoodreadsData from './hooks/use-fetch-goodreads-data'; +import { createGoodreadsEmbedLink } from './utils'; + +const GoodreadsEdit = props => { + const { attributes, className, noticeOperations, noticeUI, setAttributes } = props; + const [ userInput, setUserInput ] = useState( '' ); + const [ displayPreview, setDisplayPreview ] = useState( false ); + const [ url, setUrl ] = useState( '' ); + const [ isResolvingUrl, setIsResolvingUrl ] = useState( false ); + const prevPropsRef = useRef( null ); + + const { isFetchingData, goodreadsUserId, isError } = useFetchGoodreadsData( url ); + + useEffect( () => { + if ( attributes.link ) { + setDisplayPreview( true ); + } + }, [ attributes.link ] ); + + useEffect( () => { + if ( isFetchingData ) { + setIsResolvingUrl( true ); + } + + if ( ! isFetchingData ) { + setIsResolvingUrl( false ); + + if ( isError ) { + setAttributes( { widgetId: undefined, goodreadsId: undefined, link: undefined } ); + setErrorNotice(); + setDisplayPreview( false ); + } + + if ( goodreadsUserId && ! isError ) { + setAttributes( { goodreadsId: goodreadsUserId.toString() } ); + setRequestLink(); + setDisplayPreview( true ); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ goodreadsUserId, isFetchingData, isResolvingUrl, isError, setAttributes ] ); + + useEffect( () => { + if ( + prevPropsRef.current && + attributes.widgetId === prevPropsRef.current.attributes.widgetId + ) { + setRequestLink(); + } + prevPropsRef.current = props; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ props, attributes.widgetId ] ); + + const setErrorNotice = () => { + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( + <>{ __( 'Sorry, this content could not be embedded.', 'jetpack' ) } + ); + }; + + const setRequestLink = () => { + const selector = attributes.style === 'grid' ? 'gr_grid_widget_' : 'gr_custom_widget_'; + setAttributes( { + widgetId: Math.floor( Math.random() * 9999999 ), + link: createGoodreadsEmbedLink( { attributes } ), + id: selector + attributes.widgetId, + } ); + }; + + const submitForm = event => { + if ( event ) { + event.preventDefault(); + } + + setUrl( userInput ); + setIsResolvingUrl( true ); + }; + + const renderLoading = () => { + return ( +
+ +

{ __( 'Embedding…', 'jetpack' ) }

+
+ ); + }; + + const renderEditEmbed = () => { + return ( +
+ +
+ setUserInput( event.target.value ) } + /> + +
+
+
+ ); + }; + + const renderInlinePreview = () => { + const { goodreadsId, link, id } = attributes; + + if ( ! goodreadsId ) { + return; + } + + const html = ` + + +
+ `; + + return ( +
+ +
+
+ ); + }; + + if ( isResolvingUrl ) { + return renderLoading(); + } + + // Example block in preview. + if ( attributes.goodreadsId === 1176283 ) { + return renderInlinePreview(); + } + + if ( displayPreview ) { + return ( + <> + + + + + + + + + { renderInlinePreview() } + + ); + } + + return renderEditEmbed(); +}; + +export default withNotices( GoodreadsEdit ); diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/editor.js b/projects/plugins/jetpack/extensions/blocks/goodreads/editor.js new file mode 100644 index 0000000000000..e8a82cafbd3e9 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/editor.js @@ -0,0 +1,9 @@ +import { registerJetpackBlockFromMetadata } from '../../shared/register-jetpack-block'; +import metadata from './block.json'; +import edit from './edit'; +import save from './save'; + +registerJetpackBlockFromMetadata( metadata, { + edit, + save, +} ); diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/goodreads.php b/projects/plugins/jetpack/extensions/blocks/goodreads/goodreads.php new file mode 100644 index 0000000000000..0bb24b878aef1 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/goodreads.php @@ -0,0 +1,50 @@ + __NAMESPACE__ . '\load_assets' ) + ); +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Goodreads block registration/dependency declaration. + * + * @param array $attr Array containing the Goodreads block attributes. + * + * @return string + */ +function load_assets( $attr ) { + /* + * Enqueue necessary scripts and styles. + */ + Jetpack_Gutenberg::load_assets_as_required( __DIR__ ); + + if ( isset( $attr['link'] ) ) { + wp_enqueue_script( 'goodreads-block', $attr['link'], array(), JETPACK__VERSION, true ); + } + + return sprintf( + '
', + esc_attr( $attr['id'] ), + esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ) + ); +} diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/hooks/use-fetch-goodreads-data.js b/projects/plugins/jetpack/extensions/blocks/goodreads/hooks/use-fetch-goodreads-data.js new file mode 100644 index 0000000000000..c1e8f1a3ddf33 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/hooks/use-fetch-goodreads-data.js @@ -0,0 +1,76 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { useState, useEffect } from '@wordpress/element'; +import testEmbedUrl from '../../../shared/test-embed-url'; + +export default function useFetchGoodreadsData( input ) { + const [ goodreadsUserId, setGoodreadsUserId ] = useState( false ); + const [ isFetchingData, setIsFetchingData ] = useState( false ); + const [ isError, setIsError ] = useState( false ); + const [ is404, setIs404 ] = useState( false ); + + const fetchData = async goodreadsId => { + if ( /\/author\//.test( input ) ) { + const path = `/wpcom/v2/goodreads/user-id?id=${ goodreadsId }`; + + await apiFetch( { + path, + method: 'GET', + } ) + .then( response => { + setGoodreadsUserId( response ); + } ) + .catch( () => { + setIs404( true ); + } ) + .finally( () => { + setIsFetchingData( false ); + } ); + } else { + testEmbedUrl( input ) + .then( response => { + if ( response.endsWith( '/author' ) ) { + setIs404( true ); + } + + setGoodreadsUserId( goodreadsId ); + } ) + .catch( () => { + setIs404( true ); + } ) + .finally( () => { + setIsFetchingData( false ); + } ); + } + }; + + useEffect( () => { + // Needs to be reset because user can edit URLs. + setIsError( false ); + setIs404( false ); + + if ( input.length && ! /^(https?:\/\/)?(www\.)?goodreads\.com\/.*/.test( input ) ) { + setIsError( true ); + } + + const regex = /\/(user|author)\/show\/(\d+)/; + const goodreadsId = input.match( regex ) ? input.match( regex )[ 2 ] : false; + + if ( input.length && ! goodreadsId ) { + setIsError( true ); + } + + if ( ! isError && input.length ) { + setIsFetchingData( true ); + fetchData( goodreadsId ); + } + }, [ input, isError ] ); // eslint-disable-line react-hooks/exhaustive-deps + + return { + isFetchingData, + goodreadsUserId, + isError: isError || is404, + }; +} diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/save.js b/projects/plugins/jetpack/extensions/blocks/goodreads/save.js new file mode 100644 index 0000000000000..5bb633b942174 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/save.js @@ -0,0 +1,10 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import { InnerBlocks } from '@wordpress/block-editor'; + +export default function save() { + return ( +
+ +
+ ); +} diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/style.scss b/projects/plugins/jetpack/extensions/blocks/goodreads/style.scss new file mode 100644 index 0000000000000..02c6bd0bb4cb9 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/style.scss @@ -0,0 +1,91 @@ +@import '../../shared/styles/jetpack-variables.scss'; + +.wp-block-jetpack-goodreads { + margin-bottom: $jetpack-block-margin-bottom; + + // These styles are directly from Goodreads and + // shared with the Goodreads Widget in Jetpack. + [class^='gr_custom_container_'] { + border: 1px solid gray; + border-radius: 10px; + margin: auto; + padding: 0 5px 10px 5px; + background-color: #fff; + color: #000; + width: 300px; + } + + [class^='gr_custom_header_'] { + border-bottom: 1px solid gray; + width: 100%; + padding: 10px 0; + margin: auto; + text-align: center; + font-size: 120%; + } + + [class^='gr_custom_each_container_'] { + width: 100%; + clear: both; + margin: auto; + overflow: auto; + padding-bottom: 4px; + border-bottom: 1px solid #aaa; + } + + [class^='gr_custom_each_container_'] { + width: 100%; + clear: both; + margin-bottom: 10px; + overflow: auto; + padding-bottom: 4px; + border-bottom: 1px solid #aaa; + } + + [class^='gr_custom_book_container_'] { + overflow: hidden; + height: 60px; + float: left; + margin-right: 6px; + width: 39px; + } + + [class^='gr_custom_author_'] { + font-size: 10px; + } + + [class^='gr_custom_tags_'] { + font-size: 10px; + color: gray; + } + + [class^='gr_custom_rating_'] { + float: right; + } + + [class^='gr_grid_book_container'] { + float: left; + width: 98px; + height: 160px; + padding: 0px 0px; + overflow: hidden; + + img { + height: 100%; + width: 100%; + } + } + + // This isn't from Goodreads, but it looks better on most themes. + a { + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + img { + max-width: 100%; + } +} diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/test/controls.js b/projects/plugins/jetpack/extensions/blocks/goodreads/test/controls.js new file mode 100644 index 0000000000000..cb43118d29190 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/test/controls.js @@ -0,0 +1,163 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { GoodreadsBlockControls, GoodreadsInspectorControls } from '../controls'; + +describe( 'GoodreadsControls', () => { + const defaultAttributes = { + bookNumber: 5, + customTitle: 'My Bookshelf', + goodreadsId: '1176283', + id: 'gr_custom_widget_4529663', + link: 'https://www.goodreads.com/review/custom_widget/1176283.My Bookshelf?num_books=5&order=a&shelf=read&show_author=1&show_cover=1&show_rating=1&show_review=0&show_tags=0&show_title=1&sort=date_added&widget_id=4529663', + orderOption: 'a', + shelfOption: 'read', + showAuthor: true, + showCover: true, + showRating: true, + showReview: false, + showTags: false, + showTitle: true, + sortOption: 'date_added', + style: 'default', + widgetId: 4529663, + }; + + const setAttributes = jest.fn(); + const setDisplayPreview = jest.fn(); + const defaultProps = { + attributes: defaultAttributes, + setAttributes, + setDisplayPreview, + }; + + beforeEach( () => { + setAttributes.mockClear(); + } ); + + describe( 'Inspector settings', () => { + test( 'should update ShelfOption settings', async () => { + const user = userEvent.setup(); + render( ); + const selectElement = screen.getAllByLabelText( 'Shelf' )[ 0 ]; + await user.selectOptions( selectElement, 'Currently reading' ); + + expect( setAttributes ).toHaveBeenCalledWith( { shelfOption: 'currently-reading' } ); + } ); + + test( 'should update customTitle settings', async () => { + const user = userEvent.setup(); + render( ); + const input = screen.getAllByLabelText( 'Title' )[ 0 ]; + await user.type( input, '!' ); + + expect( setAttributes ).toHaveBeenCalledWith( { customTitle: 'My Bookshelf!' } ); + } ); + + test( 'should update sortOption settings', async () => { + const user = userEvent.setup(); + render( ); + const selectElement = screen.getAllByLabelText( 'Sort by' )[ 0 ]; + await user.selectOptions( selectElement, 'Cover' ); + + expect( setAttributes ).toHaveBeenCalledWith( { sortOption: 'cover' } ); + } ); + + test( 'should update orderOption settings', async () => { + const user = userEvent.setup(); + render( ); + const selectElement = screen.getAllByLabelText( 'Order' )[ 0 ]; + await user.selectOptions( selectElement, 'Descending' ); + + expect( setAttributes ).toHaveBeenCalledWith( { orderOption: 'd' } ); + } ); + + test( 'should update bookNumber settings', async () => { + const user = userEvent.setup(); + render( ); + const input = screen.getAllByLabelText( 'Number of books' )[ 0 ]; + await user.type( input, '0' ); + + expect( setAttributes ).toHaveBeenCalledWith( { bookNumber: '50' } ); + } ); + + test( 'offer display settings', () => { + render( ); + + expect( screen.getByText( 'Show cover' ) ).toBeInTheDocument(); + } ); + + test( 'should update showCover settings', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Show cover' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { showCover: false } ); + } ); + + test( 'should update showAuthor settings', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Show author' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { showAuthor: false } ); + } ); + + test( 'should update showTitle settings', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Show title' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { showTitle: false } ); + } ); + + test( 'should update showRating settings', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Show rating' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { showRating: false } ); + } ); + + test( 'should update showReview settings', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Show review' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { showReview: true } ); + } ); + + test( 'should update showTags settings', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Show tags' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { showTags: true } ); + } ); + + test( 'hide display settings for grid', () => { + const attributes = { ...defaultAttributes, ...{ style: 'grid' } }; + render( ); + + expect( screen.queryByText( 'Show cover' ) ).not.toBeInTheDocument(); + } ); + } ); + + describe( 'Toolbar settings', () => { + const props = { ...defaultProps, context: 'toolbar' }; + + test( 'loads and displays layout buttons in toolbar', () => { + render( ); + + expect( screen.getByLabelText( 'Default view' ) ).toBeInTheDocument(); + expect( screen.getByLabelText( 'Grid view' ) ).toBeInTheDocument(); + } ); + + test( 'sets the layout attribute', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Grid view' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { style: 'grid' } ); + } ); + } ); +} ); diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/test/edit.js b/projects/plugins/jetpack/extensions/blocks/goodreads/test/edit.js new file mode 100644 index 0000000000000..cd009f9ccf932 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/test/edit.js @@ -0,0 +1,107 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import GoodreadsEdit from '../edit'; +import useFetchGoodreadsData from '../hooks/use-fetch-goodreads-data'; + +jest.mock( './../hooks/use-fetch-goodreads-data' ); + +describe( 'GoodreadsEdit', () => { + const defaultAttributes = { + bookNumber: 2, + class: '', + customTitle: 'My Bookshelf', + goodreadsId: '', + id: '', + link: '', + orderOption: 'a', + shelfOption: 'read', + showAuthor: true, + showCover: true, + showRating: true, + showReview: false, + showTags: false, + showTitle: true, + sortOption: 'date_added', + style: 'default', + userInput: '', + widgetId: 0, + }; + + const setAttributes = jest.fn(); + const removeAllNotices = jest.fn(); + const createErrorNotice = jest.fn(); + const fetchGoodreadsData = jest.fn(); + const defaultProps = { + attributes: defaultAttributes, + noticeOperations: { + removeAllNotices, + createErrorNotice, + }, + setAttributes, + fetchGoodreadsData, + }; + + beforeEach( () => { + createErrorNotice.mockClear(); + removeAllNotices.mockClear(); + setAttributes.mockClear(); + } ); + + test( 'renders placeholder by default', async () => { + useFetchGoodreadsData.mockImplementation( () => { + return { + isFetchingData: false, + goodreadsUserId: false, + isError: false, + }; + } ); + + render( ); + + await waitFor( () => { + expect( + screen.getByPlaceholderText( 'Enter a Goodreads profile URL to embed here…' ) + ).toBeInTheDocument(); + } ); + } ); + + test( 'renders spinner while embedding', async () => { + useFetchGoodreadsData.mockImplementation( () => { + return { + isFetchingData: true, + goodreadsUserId: false, + isError: false, + }; + } ); + + const attributes = { + ...defaultAttributes, + goodreadsId: '1176283', + userInput: 'https://www.goodreads.com/user/show/1176283-matt-mullenweg', + }; + render( ); + + await expect( screen.findByText( 'Embedding…' ) ).resolves.toBeInTheDocument(); + } ); + + test( 'renders preview when finished embedding', async () => { + useFetchGoodreadsData.mockImplementation( () => { + return { + isFetchingData: false, + goodreadsUserId: '100', + isError: false, + }; + } ); + + const attributes = { + ...defaultAttributes, + goodreadsId: '1176283', + userInput: 'https://www.goodreads.com/user/show/1176283-matt-mullenweg', + }; + render( ); + + let iframe; + await waitFor( () => ( iframe = screen.getByTitle( 'Goodreads' ) ) ); + + expect( iframe ).toBeInTheDocument(); + } ); +} ); diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.html b/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.html new file mode 100644 index 0000000000000..2db7edbaac131 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.html @@ -0,0 +1,3 @@ + +
+ diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.json b/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.json new file mode 100644 index 0000000000000..99d930c4109e0 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.json @@ -0,0 +1,27 @@ +[ + { + "clientId": "_clientId_0", + "name": "jetpack/goodreads", + "isValid": true, + "attributes": { + "bookNumber": "5", + "customTitle": "My Bookshelf", + "goodreadsId": "1176283", + "id": "gr_custom_widget_4529663", + "link": "https://www.goodreads.com/review/custom_widget/1176283.My Bookshelf?num_books=5&order=a&shelf=read&show_author=1&show_cover=1&show_rating=1&show_review=0&show_tags=0&show_title=1&sort=date_added&widget_id=4529663", + "orderOption": "a", + "shelfOption": "read", + "showAuthor": true, + "showCover": true, + "showRating": true, + "showReview": false, + "showTags": false, + "showTitle": true, + "sortOption": "date_added", + "style": "default", + "widgetId": 4529663 + }, + "innerBlocks": [], + "originalContent": "
" + } +] diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.parsed.json b/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.parsed.json new file mode 100644 index 0000000000000..0ae95ca97b3de --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.parsed.json @@ -0,0 +1,14 @@ +[ + { + "blockName": "jetpack/goodreads", + "attrs": { + "goodreadsId": "1176283", + "id": "gr_custom_widget_4529663", + "link": "https://www.goodreads.com/review/custom_widget/1176283.My Bookshelf?num_books=5&order=a&shelf=read&show_author=1&show_cover=1&show_rating=1&show_review=0&show_tags=0&show_title=1&sort=date_added&widget_id=4529663", + "widgetId": 4529663 + }, + "innerBlocks": [], + "innerHTML": "\n
\n", + "innerContent": [ "\n
\n" ] + } +] diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.serialized.html b/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.serialized.html new file mode 100644 index 0000000000000..2db7edbaac131 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/test/fixtures/jetpack__goodreads.serialized.html @@ -0,0 +1,3 @@ + +
+ diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/test/utils.js b/projects/plugins/jetpack/extensions/blocks/goodreads/test/utils.js new file mode 100644 index 0000000000000..ac8a45f07cc9c --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/test/utils.js @@ -0,0 +1,28 @@ +import { createGoodreadsEmbedLink } from '../utils'; + +describe( 'GoodreadsUtils', () => { + const attributes = { + bookNumber: 5, + customTitle: 'My Bookshelf', + goodreadsId: '1176283', + id: 'gr_custom_widget_4529663', + link: 'https://www.goodreads.com/review/custom_widget/1176283.My Bookshelf?num_books=5&order=a&shelf=read&show_author=1&show_cover=1&show_rating=1&show_review=0&show_tags=0&show_title=1&sort=date_added&widget_id=4529663', + orderOption: 'a', + shelfOption: 'read', + showAuthor: true, + showCover: true, + showRating: true, + showReview: false, + showTags: false, + showTitle: true, + sortOption: 'date_added', + style: 'default', + widgetId: 4529663, + }; + + test( 'should correctly form embed link based on attributes', async () => { + expect( createGoodreadsEmbedLink( { attributes } ) ).toBe( + 'https://www.goodreads.com/review/custom_widget/1176283.My Bookshelf?num_books=5&order=a&shelf=read&show_author=1&show_cover=1&show_rating=1&show_review=0&show_tags=0&show_title=1&sort=date_added&widget_id=4529663' + ); + } ); +} ); diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/test/validate.js b/projects/plugins/jetpack/extensions/blocks/goodreads/test/validate.js new file mode 100644 index 0000000000000..045386a1348e6 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/test/validate.js @@ -0,0 +1,9 @@ +import runBlockFixtureTests from '../../../shared/test/block-fixtures'; +import metadata from '../block.json'; +import edit from '../edit'; +import save from '../save'; + +const { name } = metadata; +const blocks = [ { name, settings: { ...metadata, save, edit } } ]; + +runBlockFixtureTests( name, blocks, __dirname ); diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/utils.js b/projects/plugins/jetpack/extensions/blocks/goodreads/utils.js new file mode 100644 index 0000000000000..a9fcdbbc80779 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/utils.js @@ -0,0 +1,87 @@ +import { __, _x } from '@wordpress/i18n'; + +export const GOODREADS_SHELF_OPTIONS = [ + { + label: _x( 'Read', 'perfect participle - eg. I read a book yesterday.', 'jetpack' ), + value: 'read', + }, + { label: __( 'Currently reading', 'jetpack' ), value: 'currently-reading' }, + { + label: _x( 'To read', 'future participle - eg. I have this to read tomorrow.', 'jetpack' ), + value: 'to-read', + }, +]; + +export const GOODREADS_SORT_OPTIONS = [ + { label: 'ASIN', value: 'asin' }, + { label: _x( 'Author', 'noun', 'jetpack' ), value: 'author' }, + { label: __( 'Average rating', 'jetpack' ), value: 'avg_rating' }, + { label: _x( 'Comments', 'noun', 'jetpack' ), value: 'comments' }, + { label: _x( 'Cover', 'noun - ie. book cover', 'jetpack' ), value: 'cover' }, + { label: __( 'Date added', 'jetpack' ), value: 'date_added' }, + { label: __( 'Date published', 'jetpack' ), value: 'date_pub' }, + { label: __( 'Date read', 'jetpack' ), value: 'date_read' }, + { label: __( 'Date started', 'jetpack' ), value: 'date_started' }, + { label: __( 'Dated updated', 'jetpack' ), value: 'date_updated' }, + { label: _x( 'Format', 'noun', 'jetpack' ), value: 'format' }, + { label: 'ISBN', value: 'isbn' }, + { label: 'ISBN-13', value: 'isbn13' }, + { label: _x( 'Notes', 'noun', 'jetpack' ), value: 'notes' }, + { label: __( 'Number of pages', 'jetpack' ), value: 'num_pages' }, + { label: __( 'Number of ratings', 'jetpack' ), value: 'num_ratings' }, + { + label: _x( 'Owned', 'possessive - eg. I owned this book for a year', 'jetpack' ), + value: 'owned', + }, + { label: _x( 'Position', 'noun', 'jetpack' ), value: 'position' }, + { label: __( 'Random', 'jetpack', 'jetpack' ), value: 'random' }, + { label: _x( 'Rating', 'noun', 'jetpack' ), value: 'rating' }, + { label: __( 'Read count', 'jetpack' ), value: 'read_count' }, + { label: _x( 'Review', 'noun', 'jetpack' ), value: 'review' }, + { label: _x( 'Shelves', 'noun', 'jetpack' ), value: 'shelves' }, + { label: _x( 'Title', 'noun', 'jetpack' ), value: 'title' }, + { label: _x( 'Votes', 'noun', 'jetpack' ), value: 'votes' }, + { label: __( 'Year published', 'jetpack' ), value: 'year_pub' }, +]; + +export const GOODREADS_ORDER_OPTIONS = [ + { label: __( 'Ascending', 'jetpack' ), value: 'a' }, + { label: __( 'Descending', 'jetpack' ), value: 'd' }, +]; + +export function createGoodreadsEmbedLink( { attributes } ) { + const { + bookNumber, + customTitle, + goodreadsId, + orderOption, + shelfOption, + showAuthor, + showCover, + showRating, + showReview, + showTags, + showTitle, + sortOption, + style, + widgetId, + } = attributes; + + if ( ! goodreadsId ) { + return; + } + + let link = `https://www.goodreads.com/review/custom_widget/${ goodreadsId }.${ customTitle }?num_books=${ bookNumber }&order=${ orderOption }&shelf=${ shelfOption }&show_author=${ + showAuthor ? 1 : 0 + }&show_cover=${ showCover ? 1 : 0 }&show_rating=${ showRating ? 1 : 0 }&show_review=${ + showReview ? 1 : 0 + }&show_tags=${ showTags ? 1 : 0 }&show_title=${ + showTitle ? 1 : 0 + }&sort=${ sortOption }&widget_id=${ widgetId }`; + + if ( style === 'grid' ) { + link = `https://www.goodreads.com/review/grid_widget/${ goodreadsId }.${ customTitle }?cover_size=medium&num_books=${ bookNumber }&order=${ orderOption }&shelf=${ shelfOption }&sort=${ sortOption }&widget_id=${ widgetId }`; + } + + return link; +} diff --git a/projects/plugins/jetpack/extensions/blocks/goodreads/view.js b/projects/plugins/jetpack/extensions/blocks/goodreads/view.js new file mode 100644 index 0000000000000..423b033ce717c --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/goodreads/view.js @@ -0,0 +1 @@ +import './style.scss'; diff --git a/projects/plugins/jetpack/extensions/index.json b/projects/plugins/jetpack/extensions/index.json index 430cde191423b..e6a3c7d3696ce 100644 --- a/projects/plugins/jetpack/extensions/index.json +++ b/projects/plugins/jetpack/extensions/index.json @@ -67,6 +67,7 @@ "google-docs-embed", "launchpad-save-modal", "recipe", + "goodreads", "v6-video-frame-poster", "videopress/video-chapters", "voice-to-content", diff --git a/projects/plugins/jetpack/modules/widgets/goodreads.php b/projects/plugins/jetpack/modules/widgets/goodreads.php index 81a35d5874964..b15c1a4bb77fc 100644 --- a/projects/plugins/jetpack/modules/widgets/goodreads.php +++ b/projects/plugins/jetpack/modules/widgets/goodreads.php @@ -47,6 +47,18 @@ public function __construct() { if ( is_active_widget( '', '', 'wpcom-goodreads' ) || is_customize_preview() ) { add_action( 'wp_print_styles', array( $this, 'enqueue_style' ) ); } + add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_widget_in_block_editor' ) ); + } + + /** + * Remove the "Goodreads" widget from the Legacy Widget block + * + * @param array $widget_types List of widgets that are currently removed from the Legacy Widget block. + * @return array $widget_types New list of widgets that will be removed. + */ + public function hide_widget_in_block_editor( $widget_types ) { + $widget_types[] = 'wpcom-goodreads'; + return $widget_types; } /**