@automattic/newspack-blocks 4.31.1 → 4.32.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/composer.lock +73 -994
- package/dist/blocks/checkout-button/block.json +3 -0
- package/dist/carousel/view-rtl.css +1 -1
- package/dist/carousel/view.asset.php +1 -1
- package/dist/carousel/view.css +1 -1
- package/dist/editor-rtl.css +20 -16
- package/dist/editor.asset.php +1 -1
- package/dist/editor.css +20 -16
- package/dist/editor.js +32 -28
- package/dist/homepage-articles/view-rtl.css +1 -1
- package/dist/homepage-articles/view.asset.php +1 -1
- package/dist/homepage-articles/view.css +1 -1
- package/dist/modal.asset.php +1 -1
- package/dist/modal.js +1 -1
- package/dist/modalCheckout-rtl.css +1 -1
- package/dist/modalCheckout.asset.php +1 -1
- package/dist/modalCheckout.css +1 -1
- package/dist/modalCheckout.js +1 -1
- package/dist/tiersBased.asset.php +1 -1
- package/dist/tiersBased.js +1 -1
- package/includes/class-modal-checkout.php +354 -5
- package/includes/modal-checkout/class-checkout-data.php +30 -6
- package/languages/newspack-blocks-de_DE-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-de_DE-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-de_DE-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-de_DE-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-de_DE-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-de_DE-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-de_DE.po +427 -322
- package/languages/newspack-blocks-es_ES-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-es_ES-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-es_ES-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-es_ES-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-es_ES-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-es_ES-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-es_ES.po +425 -322
- package/languages/newspack-blocks-fr_BE-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-fr_BE-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-fr_BE-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-fr_BE-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-fr_BE-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-fr_BE-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-fr_BE.po +427 -322
- package/languages/newspack-blocks-nb_NO-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-nb_NO-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-nb_NO-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-nb_NO-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-nb_NO-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-nb_NO-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-nb_NO.po +426 -322
- package/languages/newspack-blocks-pt_PT-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-pt_PT-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-pt_PT-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-pt_PT-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-pt_PT-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-pt_PT-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-pt_PT.po +426 -322
- package/languages/newspack-blocks.pot +506 -379
- package/newspack-blocks.php +2 -2
- package/package.json +3 -2
- package/src/blocks/carousel/edit.js +1 -1
- package/src/blocks/carousel/view.scss +5 -1
- package/src/blocks/checkout-button/block.json +3 -0
- package/src/blocks/checkout-button/edit.js +32 -5
- package/src/blocks/checkout-button/view.php +37 -0
- package/src/blocks/donate/consts.ts +2 -1
- package/src/blocks/donate/frequency-slugs.js +10 -0
- package/src/blocks/donate/tiers-based/view.ts +8 -0
- package/src/blocks/homepage-articles/edit.tsx +1 -1
- package/src/blocks/homepage-articles/editor.scss +2 -1
- package/src/blocks/homepage-articles/view.scss +15 -5
- package/src/components/autocomplete-tokenfield.js +21 -7
- package/src/components/autocomplete-tokenfield.scss +1 -1
- package/src/components/autocomplete-tokenfield.test.js +58 -0
- package/src/components/query-controls.js +28 -6
- package/src/components/query-controls.test.js +56 -0
- package/src/components/reorder-modal.js +382 -0
- package/src/components/reorder-modal.scss +87 -0
- package/src/components/reorder-modal.test.js +487 -0
- package/src/components/specific-posts-control.js +91 -0
- package/src/components/specific-posts-control.scss +5 -0
- package/src/components/specific-posts-control.test.js +173 -0
- package/src/modal-checkout/analytics/ga4/utils/index.js +1 -0
- package/src/modal-checkout/checkout-button-trigger.js +29 -2
- package/src/modal-checkout/checkout-button-trigger.test.js +37 -0
- package/src/modal-checkout/checkout.scss +28 -0
- package/src/modal-checkout/donate-trigger.js +134 -0
- package/src/modal-checkout/donate-trigger.test.js +233 -0
- package/src/modal-checkout/index.js +91 -0
- package/src/modal-checkout/modal.js +129 -45
- package/src/modal-checkout/utils.test.js +49 -0
- package/src/shared/js/tiers-based-ready.js +26 -0
- package/vendor/autoload.php +1 -1
- package/vendor/composer/autoload_real.php +4 -4
- package/vendor/composer/autoload_static.php +2 -2
- package/vendor/composer/installed.php +2 -2
package/newspack-blocks.php
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Author URI: https://newspack.com/
|
|
8
8
|
* Text Domain: newspack-blocks
|
|
9
9
|
* Domain Path: /languages
|
|
10
|
-
* Version: 4.
|
|
10
|
+
* Version: 4.32.0-alpha.2
|
|
11
11
|
*
|
|
12
12
|
* @package Newspack_Blocks
|
|
13
13
|
*/
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
define( 'NEWSPACK_BLOCKS__PLUGIN_FILE', __FILE__ );
|
|
16
16
|
define( 'NEWSPACK_BLOCKS__BLOCKS_DIRECTORY', 'dist/' );
|
|
17
17
|
define( 'NEWSPACK_BLOCKS__PLUGIN_DIR', plugin_dir_path( NEWSPACK_BLOCKS__PLUGIN_FILE ) );
|
|
18
|
-
define( 'NEWSPACK_BLOCKS__VERSION', '4.
|
|
18
|
+
define( 'NEWSPACK_BLOCKS__VERSION', '4.32.0-alpha.2' );
|
|
19
19
|
|
|
20
20
|
require_once NEWSPACK_BLOCKS__PLUGIN_DIR . 'includes/class-newspack-blocks.php';
|
|
21
21
|
require_once NEWSPACK_BLOCKS__PLUGIN_DIR . 'includes/class-newspack-blocks-api.php';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/newspack-blocks",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.32.0-alpha.2",
|
|
4
4
|
"author": "Automattic",
|
|
5
5
|
"description": "=== Newspack Blocks === Contributors: (this should be a list of wordpress.org userid's) Donate link: https://example.com/ Tags: comments, spam Requires at least: 4.5 Tested up to: 5.1.1 Stable tag: 0.1.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html",
|
|
6
6
|
"repository": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"extends @wordpress/browserslist-config"
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@wordpress/ui": "^0.16.0",
|
|
48
49
|
"classnames": "^2.5.1",
|
|
49
50
|
"lodash": "^4.17.21",
|
|
50
51
|
"newspack-colors": "1.1.4",
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/lodash": "^4.17.23",
|
|
59
|
-
"newspack-components": "4.
|
|
60
|
+
"newspack-components": "4.8.0-alpha.1",
|
|
60
61
|
"newspack-scripts": "5.11.0"
|
|
61
62
|
},
|
|
62
63
|
"overrides": {
|
|
@@ -413,7 +413,7 @@ class Edit extends Component {
|
|
|
413
413
|
</div>
|
|
414
414
|
) }
|
|
415
415
|
{ showTagLabels && post.newspack_tag_labels && (
|
|
416
|
-
<div className="
|
|
416
|
+
<div className="tag-labels">
|
|
417
417
|
{ post.newspack_tag_labels.map( ( newspack_tag_label, index ) => {
|
|
418
418
|
return newspack_tag_label.link ? (
|
|
419
419
|
<a key={ index } href="#" className="tag-label flag">
|
|
@@ -263,7 +263,11 @@
|
|
|
263
263
|
margin-right: 1.5em;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
-
.
|
|
266
|
+
// One source for the carousel's chip styling, shared so the two stay in step.
|
|
267
|
+
// A tag label carries only `tag-labels`, so a publisher rule written for
|
|
268
|
+
// `.cat-links a` cannot reach it.
|
|
269
|
+
.cat-links,
|
|
270
|
+
.tag-labels {
|
|
267
271
|
color: np-colors.$neutral-000;
|
|
268
272
|
font-size: 0.6em;
|
|
269
273
|
font-weight: bold;
|
|
@@ -207,11 +207,17 @@ function ProductControl( props ) {
|
|
|
207
207
|
|
|
208
208
|
function CheckoutButtonEdit( props ) {
|
|
209
209
|
const { attributes, setAttributes, className } = props;
|
|
210
|
-
const { placeholder, style, text, product, price, variation, width, coupon } = attributes;
|
|
210
|
+
const { placeholder, style, text, product, price, variation, width, coupon, quantity } = attributes;
|
|
211
211
|
|
|
212
212
|
const [ productData, setProductData ] = useState( {} );
|
|
213
213
|
const [ variations, setVariations ] = useState( [] );
|
|
214
214
|
const [ nyp, setNYP ] = useState( false );
|
|
215
|
+
// Resolved server-side, like the donation flag: a seat count is only honoured
|
|
216
|
+
// for a product sold per seat, so offering the control anywhere else would
|
|
217
|
+
// invite a number that is discarded at checkout. Per-seat meta lives on the
|
|
218
|
+
// variation for a tiered plan, hence the separate state for the chosen one.
|
|
219
|
+
const [ variationHasSeats, setVariationHasSeats ] = useState( false );
|
|
220
|
+
const hasSeats = variation ? variationHasSeats : !! productData?.newspack_has_seats;
|
|
215
221
|
|
|
216
222
|
// Resolved server-side, since a donation is not always identifiable from the
|
|
217
223
|
// product meta. Variations inherit it from their parent, which is the product
|
|
@@ -249,10 +255,17 @@ function CheckoutButtonEdit( props ) {
|
|
|
249
255
|
useEffect( () => {
|
|
250
256
|
if ( variation ) {
|
|
251
257
|
apiFetch( { path: `/wc/v2/products/${ product }/variations/${ variation }` } )
|
|
252
|
-
.then( res =>
|
|
253
|
-
|
|
258
|
+
.then( res => {
|
|
259
|
+
setNYP( getNYP( res ) );
|
|
260
|
+
setVariationHasSeats( !! res?.newspack_has_seats );
|
|
261
|
+
} )
|
|
262
|
+
.catch( () => {
|
|
263
|
+
setNYP( {} );
|
|
264
|
+
setVariationHasSeats( false );
|
|
265
|
+
} );
|
|
254
266
|
} else {
|
|
255
267
|
setNYP( getNYP( productData ) );
|
|
268
|
+
setVariationHasSeats( false );
|
|
256
269
|
}
|
|
257
270
|
}, [ variation ] );
|
|
258
271
|
|
|
@@ -312,7 +325,7 @@ function CheckoutButtonEdit( props ) {
|
|
|
312
325
|
<ProductControl
|
|
313
326
|
value={ product }
|
|
314
327
|
price={ price }
|
|
315
|
-
onChange={ value => setAttributes( { product: value, variation: '', price: '' } ) }
|
|
328
|
+
onChange={ value => setAttributes( { product: value, variation: '', price: '', quantity: undefined } ) }
|
|
316
329
|
onProduct={ handleProduct }
|
|
317
330
|
>
|
|
318
331
|
{ productData?.variations?.length > 0 && (
|
|
@@ -324,6 +337,7 @@ function CheckoutButtonEdit( props ) {
|
|
|
324
337
|
setAttributes( {
|
|
325
338
|
variation: value ? '' : variations[ 0 ].id.toString(),
|
|
326
339
|
price: '',
|
|
340
|
+
quantity: undefined,
|
|
327
341
|
} )
|
|
328
342
|
}
|
|
329
343
|
/>
|
|
@@ -340,7 +354,7 @@ function CheckoutButtonEdit( props ) {
|
|
|
340
354
|
value: item.id,
|
|
341
355
|
} ) ),
|
|
342
356
|
] }
|
|
343
|
-
onChange={ value => setAttributes( { variation: value.toString(), price: '' } ) }
|
|
357
|
+
onChange={ value => setAttributes( { variation: value.toString(), price: '', quantity: undefined } ) }
|
|
344
358
|
/>
|
|
345
359
|
) : (
|
|
346
360
|
<Spinner />
|
|
@@ -410,6 +424,19 @@ function CheckoutButtonEdit( props ) {
|
|
|
410
424
|
/>
|
|
411
425
|
</PanelBody>
|
|
412
426
|
) }
|
|
427
|
+
{ hasSeats && (
|
|
428
|
+
<PanelBody title={ __( 'Seats', 'newspack-blocks' ) } initialOpen={ false }>
|
|
429
|
+
<p>{ __( 'Optional. Pre-fill the number of seats. Readers can change it before paying.', 'newspack-blocks' ) }</p>
|
|
430
|
+
<TextControl
|
|
431
|
+
type="number"
|
|
432
|
+
label={ __( 'Default seats', 'newspack-blocks' ) }
|
|
433
|
+
value={ quantity || '' }
|
|
434
|
+
min={ 1 }
|
|
435
|
+
onChange={ value => setAttributes( { quantity: value ? parseInt( value, 10 ) : undefined } ) }
|
|
436
|
+
__next40pxDefaultSize
|
|
437
|
+
/>
|
|
438
|
+
</PanelBody>
|
|
439
|
+
) }
|
|
413
440
|
</InspectorControls>
|
|
414
441
|
</>
|
|
415
442
|
);
|
|
@@ -56,6 +56,37 @@ function register_donation_rest_field() {
|
|
|
56
56
|
}
|
|
57
57
|
add_action( 'rest_api_init', __NAMESPACE__ . '\\register_donation_rest_field' );
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Expose whether a product is sold per seat on the products REST response.
|
|
61
|
+
*
|
|
62
|
+
* The block editor needs this to decide whether to offer a default seat count:
|
|
63
|
+
* a quantity on any other product is discarded at checkout, so a control for one
|
|
64
|
+
* would invite the publisher to set a number that never takes effect.
|
|
65
|
+
*/
|
|
66
|
+
function register_seats_rest_field() {
|
|
67
|
+
if ( ! class_exists( '\Newspack\Group_Subscription_Seats' ) || ! method_exists( '\Newspack\Group_Subscription_Seats', 'get_field_args' ) ) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Both object types: per-seat meta lives on the variation for a tiered plan, and
|
|
71
|
+
// the editor fetches the chosen variation separately from its parent.
|
|
72
|
+
register_rest_field(
|
|
73
|
+
[ 'product', 'product_variation' ],
|
|
74
|
+
'newspack_has_seats',
|
|
75
|
+
[
|
|
76
|
+
'get_callback' => function ( $product ) {
|
|
77
|
+
return ! empty( \Newspack\Group_Subscription_Seats::get_field_args( $product['id'] ) );
|
|
78
|
+
},
|
|
79
|
+
'schema' => [
|
|
80
|
+
'description' => __( 'Whether the product is sold per seat, so a seat count can be chosen for it.', 'newspack-blocks' ),
|
|
81
|
+
'type' => 'boolean',
|
|
82
|
+
'context' => [ 'view', 'edit' ],
|
|
83
|
+
'readonly' => true,
|
|
84
|
+
],
|
|
85
|
+
]
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
add_action( 'rest_api_init', __NAMESPACE__ . '\\register_seats_rest_field' );
|
|
89
|
+
|
|
59
90
|
/**
|
|
60
91
|
* Render the block.
|
|
61
92
|
*
|
|
@@ -95,6 +126,7 @@ function render_callback( $attributes ) {
|
|
|
95
126
|
$after_success_button_label = $attributes['afterSuccessButtonLabel'] ?? '';
|
|
96
127
|
$after_success_url = $attributes['afterSuccessURL'] ?? '';
|
|
97
128
|
$coupon = $attributes['coupon'] ?? '';
|
|
129
|
+
$quantity = absint( $attributes['quantity'] ?? 0 );
|
|
98
130
|
$is_variable = $attributes['is_variable'];
|
|
99
131
|
|
|
100
132
|
if ( $is_variable && $variation_id ) {
|
|
@@ -157,6 +189,11 @@ function render_callback( $attributes ) {
|
|
|
157
189
|
if ( '' !== $coupon ) {
|
|
158
190
|
$hidden_fields .= '<input type="hidden" name="coupon" value="' . esc_attr( $coupon ) . '" />';
|
|
159
191
|
}
|
|
192
|
+
// Only emitted above 1: the server already defaults to a single seat, and a
|
|
193
|
+
// reader-editable field inside the modal wins over this default anyway.
|
|
194
|
+
if ( $quantity > 1 ) {
|
|
195
|
+
$hidden_fields .= '<input type="hidden" name="quantity" value="' . esc_attr( $quantity ) . '" />';
|
|
196
|
+
}
|
|
160
197
|
|
|
161
198
|
ob_start();
|
|
162
199
|
/**
|
|
@@ -7,6 +7,7 @@ import { __ } from '@wordpress/i18n';
|
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
9
|
import type { DonationFrequencySlug, DonateBlockAttributes } from './types';
|
|
10
|
+
import { FREQUENCY_SLUGS as DONATE_FREQUENCY_SLUGS } from './frequency-slugs';
|
|
10
11
|
|
|
11
12
|
export const FREQUENCIES: Record< DonationFrequencySlug, string > = {
|
|
12
13
|
once: __( 'One-time', 'newspack-blocks' ),
|
|
@@ -14,7 +15,7 @@ export const FREQUENCIES: Record< DonationFrequencySlug, string > = {
|
|
|
14
15
|
year: __( 'Annually', 'newspack-blocks' ),
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
export const FREQUENCY_SLUGS
|
|
18
|
+
export const FREQUENCY_SLUGS = DONATE_FREQUENCY_SLUGS as DonationFrequencySlug[];
|
|
18
19
|
|
|
19
20
|
export const LAYOUT_OPTIONS: { label: string; key: DonateBlockAttributes[ 'layoutOption' ] }[] = [
|
|
20
21
|
{ label: __( 'Frequency', 'newspack-blocks' ), key: 'frequency' },
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Donation frequency slugs, on their own so any bundle can consume them:
|
|
3
|
+
* consts.ts attaches translated labels (pulling in @wordpress/i18n at module
|
|
4
|
+
* scope), which the modal checkout's donate-trigger resolver must not drag
|
|
5
|
+
* into its bundles just to validate trigger params. The PHP renderers
|
|
6
|
+
* hardcode the same set (class-newspack-blocks-donate-renderer-base.php).
|
|
7
|
+
*
|
|
8
|
+
* @type {string[]}
|
|
9
|
+
*/
|
|
10
|
+
export const FREQUENCY_SLUGS = [ 'once', 'month', 'year' ];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setupSlider } from './slider';
|
|
2
2
|
import { parseTiersBasedConfig } from './utils';
|
|
3
|
+
import { TIERS_BASED_READY_EVENT, TIERS_BASED_READY_ATTRIBUTE } from '../../../shared/js/tiers-based-ready';
|
|
3
4
|
|
|
4
5
|
const BUTTON_ACTIVE_CLASSNAME = 'wpbnbd__button--active';
|
|
5
6
|
|
|
@@ -129,4 +130,11 @@ export default ( parentEl: HTMLElement ) => {
|
|
|
129
130
|
backButton.style.display = 'none';
|
|
130
131
|
}
|
|
131
132
|
} );
|
|
133
|
+
|
|
134
|
+
// Mark the block as ready for programmatic triggering (see
|
|
135
|
+
// src/modal-checkout/donate-trigger.js): the listeners above are attached,
|
|
136
|
+
// so a tab click now synchronously updates the tier submit buttons. The
|
|
137
|
+
// event lets a URL trigger that ran before this init retry immediately.
|
|
138
|
+
parentEl.setAttribute( TIERS_BASED_READY_ATTRIBUTE, '' );
|
|
139
|
+
parentEl.dispatchEvent( new CustomEvent( TIERS_BASED_READY_EVENT, { bubbles: true } ) );
|
|
132
140
|
};
|
|
@@ -115,7 +115,7 @@ class Edit extends Component< HomepageArticlesProps > {
|
|
|
115
115
|
</div>
|
|
116
116
|
) }{ ' ' }
|
|
117
117
|
{ showTagLabels && post.newspack_tag_labels && (
|
|
118
|
-
<div className="
|
|
118
|
+
<div className="tag-labels">
|
|
119
119
|
{ post.newspack_tag_labels.map( ( newspack_tag_label, index ) => {
|
|
120
120
|
return newspack_tag_label.link ? (
|
|
121
121
|
<a key={ index } href="#" className="tag-label flag">
|
|
@@ -271,6 +271,7 @@
|
|
|
271
271
|
.cat-links,
|
|
272
272
|
.cat-links a,
|
|
273
273
|
.cat-links a:visited,
|
|
274
|
+
.tag-labels,
|
|
274
275
|
figcaption {
|
|
275
276
|
color: inherit;
|
|
276
277
|
}
|
|
@@ -429,7 +430,8 @@
|
|
|
429
430
|
&.has-text-align-center {
|
|
430
431
|
.cat-links,
|
|
431
432
|
.entry-meta,
|
|
432
|
-
.entry-sponsors
|
|
433
|
+
.entry-sponsors,
|
|
434
|
+
.tag-labels {
|
|
433
435
|
justify-content: center;
|
|
434
436
|
}
|
|
435
437
|
|
|
@@ -444,7 +446,8 @@
|
|
|
444
446
|
.cat-links,
|
|
445
447
|
.entry-meta,
|
|
446
448
|
.entry-sponsors,
|
|
447
|
-
.sponsor-logos
|
|
449
|
+
.sponsor-logos,
|
|
450
|
+
.tag-labels {
|
|
448
451
|
justify-content: flex-end;
|
|
449
452
|
text-align: right;
|
|
450
453
|
}
|
|
@@ -463,7 +466,12 @@
|
|
|
463
466
|
}
|
|
464
467
|
|
|
465
468
|
/* Article meta */
|
|
466
|
-
|
|
469
|
+
// `.tag-labels` shares this rule rather than repeating it, so an edit to one
|
|
470
|
+
// cannot silently diverge from the other. The wrapper carries only
|
|
471
|
+
// `tag-labels`, which is what keeps a publisher's per-section `.cat-links a`
|
|
472
|
+
// rule from reaching a tag label; sharing layout here does not give that back.
|
|
473
|
+
.cat-links,
|
|
474
|
+
.tag-labels {
|
|
467
475
|
display: flex;
|
|
468
476
|
flex-wrap: wrap;
|
|
469
477
|
font-size: variables.$font__size-xxs;
|
|
@@ -516,7 +524,8 @@
|
|
|
516
524
|
.cat-links,
|
|
517
525
|
.entry-meta,
|
|
518
526
|
.entry-sponsors,
|
|
519
|
-
.sponsor-logos
|
|
527
|
+
.sponsor-logos,
|
|
528
|
+
.tag-labels {
|
|
520
529
|
justify-content: flex-start;
|
|
521
530
|
}
|
|
522
531
|
}
|
|
@@ -525,7 +534,8 @@
|
|
|
525
534
|
.cat-links,
|
|
526
535
|
.entry-meta,
|
|
527
536
|
.entry-sponsors,
|
|
528
|
-
.sponsor-logos
|
|
537
|
+
.sponsor-logos,
|
|
538
|
+
.tag-labels {
|
|
529
539
|
justify-content: flex-end;
|
|
530
540
|
}
|
|
531
541
|
}
|
|
@@ -45,15 +45,29 @@ class AutocompleteTokenField extends Component {
|
|
|
45
45
|
if ( this.isFetchingInfoOnLoad() ) {
|
|
46
46
|
const { tokens, fetchSavedInfo } = this.props;
|
|
47
47
|
|
|
48
|
-
fetchSavedInfo( tokens )
|
|
49
|
-
|
|
48
|
+
fetchSavedInfo( tokens )
|
|
49
|
+
.then( results => {
|
|
50
|
+
const { validValues } = this.state;
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
results.forEach( suggestion => {
|
|
53
|
+
validValues[ suggestion.value ] = suggestion.label;
|
|
54
|
+
} );
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
this.setState( { validValues, loading: false } );
|
|
57
|
+
} )
|
|
58
|
+
.catch( error => {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.error( 'Newspack Blocks: could not load the titles for the saved selection.', error );
|
|
61
|
+
// Without labels the field would render no tokens, and the next
|
|
62
|
+
// selection would then write back a list missing every saved ID.
|
|
63
|
+
const { validValues } = this.state;
|
|
64
|
+
|
|
65
|
+
tokens.forEach( token => {
|
|
66
|
+
validValues[ token ] = String( token );
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
this.setState( { validValues, loading: false } );
|
|
70
|
+
} );
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import AutocompleteTokenField from './autocomplete-tokenfield';
|
|
10
|
+
|
|
11
|
+
const noop = () => {};
|
|
12
|
+
|
|
13
|
+
describe( 'AutocompleteTokenField when the saved titles cannot be loaded', () => {
|
|
14
|
+
let errorSpy;
|
|
15
|
+
|
|
16
|
+
beforeEach( () => {
|
|
17
|
+
errorSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} );
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
afterEach( () => {
|
|
21
|
+
errorSpy.mockRestore();
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
const renderField = () => {
|
|
25
|
+
const onChange = jest.fn();
|
|
26
|
+
render(
|
|
27
|
+
<AutocompleteTokenField
|
|
28
|
+
tokens={ [ 11, 22 ] }
|
|
29
|
+
onChange={ onChange }
|
|
30
|
+
fetchSuggestions={ noop }
|
|
31
|
+
fetchSavedInfo={ () => Promise.reject( new Error( 'unreachable' ) ) }
|
|
32
|
+
label="Content"
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
return { onChange };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
it( 'shows the IDs rather than dropping the saved selection', async () => {
|
|
39
|
+
renderField();
|
|
40
|
+
expect( await screen.findByText( '11' ) ).toBeInTheDocument();
|
|
41
|
+
expect( screen.getByText( '22' ) ).toBeInTheDocument();
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
// The field looks settled once the spinner goes, so the console is the only
|
|
45
|
+
// trace left of why the titles turned into numbers.
|
|
46
|
+
it( 'leaves the cause in the console', async () => {
|
|
47
|
+
renderField();
|
|
48
|
+
await screen.findByText( '11' );
|
|
49
|
+
expect( errorSpy ).toHaveBeenCalledWith( expect.stringContaining( 'could not load the titles' ), expect.any( Error ) );
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
it( 'keeps the remaining IDs when one token is removed', async () => {
|
|
53
|
+
const { onChange } = renderField();
|
|
54
|
+
await screen.findByText( '11' );
|
|
55
|
+
fireEvent.click( document.querySelectorAll( '.components-form-token-field__remove-token' )[ 0 ] );
|
|
56
|
+
expect( onChange ).toHaveBeenCalledWith( [ '22' ] );
|
|
57
|
+
} );
|
|
58
|
+
} );
|
|
@@ -22,6 +22,7 @@ import { decodeEntities } from '@wordpress/html-entities';
|
|
|
22
22
|
* Internal dependencies.
|
|
23
23
|
*/
|
|
24
24
|
import AutocompleteTokenField from './autocomplete-tokenfield';
|
|
25
|
+
import SpecificPostsControl from './specific-posts-control';
|
|
25
26
|
|
|
26
27
|
const getCategoryTitle = category => decodeEntities( category.name ) || __( '(no title)', 'newspack-blocks' );
|
|
27
28
|
|
|
@@ -50,19 +51,42 @@ class QueryControls extends Component {
|
|
|
50
51
|
fetchSavedPosts = postIDs => {
|
|
51
52
|
const { postType } = this.props;
|
|
52
53
|
const restUrl = window.newspack_blocks_data.posts_rest_url;
|
|
54
|
+
// An empty `include` is dropped server-side, which would answer with the
|
|
55
|
+
// most recent post rather than nothing.
|
|
56
|
+
if ( ! postIDs.length ) {
|
|
57
|
+
return Promise.resolve( [] );
|
|
58
|
+
}
|
|
53
59
|
return apiFetch( {
|
|
54
60
|
url: addQueryArgs( restUrl, {
|
|
55
61
|
// These params use the block query parameters (see Newspack_Blocks::build_articles_query).
|
|
56
|
-
postsToShow:
|
|
62
|
+
postsToShow: postIDs.length,
|
|
57
63
|
include: postIDs.join( ',' ),
|
|
58
64
|
_fields: 'id,title',
|
|
59
65
|
postType,
|
|
60
66
|
} ),
|
|
61
67
|
} ).then( function ( posts ) {
|
|
62
|
-
|
|
68
|
+
const allPosts = posts.map( post => ( {
|
|
63
69
|
value: post.id,
|
|
64
70
|
label: decodeEntities( post.title.rendered ) || __( '(no title)', 'newspack-blocks' ),
|
|
65
71
|
} ) );
|
|
72
|
+
// An ID the endpoint did not return still needs an entry, or the token
|
|
73
|
+
// field drops it from the block on the next edit. The ID goes in the
|
|
74
|
+
// label because labels are matched back to IDs, and a shared label
|
|
75
|
+
// would collapse two items onto one.
|
|
76
|
+
postIDs.forEach( postID => {
|
|
77
|
+
const id = parseInt( postID );
|
|
78
|
+
if ( ! allPosts.find( post => post.value === id ) ) {
|
|
79
|
+
allPosts.push( {
|
|
80
|
+
value: id,
|
|
81
|
+
label: sprintf(
|
|
82
|
+
/* translators: %d: numeric ID of content that could not be found. */
|
|
83
|
+
__( 'Unavailable content (%d)', 'newspack-blocks' ),
|
|
84
|
+
id
|
|
85
|
+
),
|
|
86
|
+
} );
|
|
87
|
+
}
|
|
88
|
+
} );
|
|
89
|
+
return allPosts;
|
|
66
90
|
} );
|
|
67
91
|
};
|
|
68
92
|
|
|
@@ -307,13 +331,11 @@ class QueryControls extends Component {
|
|
|
307
331
|
</ToggleGroupControl>
|
|
308
332
|
) }
|
|
309
333
|
{ specificMode ? (
|
|
310
|
-
<
|
|
311
|
-
|
|
334
|
+
<SpecificPostsControl
|
|
335
|
+
postIds={ specificPosts || [] }
|
|
312
336
|
onChange={ onSpecificPostsChange }
|
|
313
337
|
fetchSuggestions={ this.fetchPostSuggestions }
|
|
314
338
|
fetchSavedInfo={ this.fetchSavedPosts }
|
|
315
|
-
label={ __( 'Content', 'newspack-blocks' ) }
|
|
316
|
-
help={ __( 'Begin typing any word in a title. Click on an autocomplete result to select it.', 'newspack-blocks' ) }
|
|
317
339
|
/>
|
|
318
340
|
) : (
|
|
319
341
|
<>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import apiFetch from '@wordpress/api-fetch';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import QueryControls from './query-controls';
|
|
10
|
+
|
|
11
|
+
jest.mock( '@wordpress/api-fetch' );
|
|
12
|
+
|
|
13
|
+
const fetchSavedPosts = ( postIDs, posts ) => {
|
|
14
|
+
apiFetch.mockResolvedValue( posts );
|
|
15
|
+
return new QueryControls( { postType: 'post' } ).fetchSavedPosts( postIDs );
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe( 'fetchSavedPosts', () => {
|
|
19
|
+
beforeAll( () => {
|
|
20
|
+
window.newspack_blocks_data = { posts_rest_url: 'https://example.test/wp-json/newspack-blocks/v1/newspack-blocks-posts' };
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
beforeEach( () => {
|
|
24
|
+
apiFetch.mockClear();
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'labels the content the endpoint returned', async () => {
|
|
28
|
+
await expect( fetchSavedPosts( [ 11 ], [ { id: 11, title: { rendered: 'Alpha' } } ] ) ).resolves.toEqual( [ { value: 11, label: 'Alpha' } ] );
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
// Without an entry the token field renders no token for the ID, and the next
|
|
32
|
+
// selection writes the attribute back without it.
|
|
33
|
+
it( 'keeps an entry for an ID the endpoint did not return', async () => {
|
|
34
|
+
await expect( fetchSavedPosts( [ 11, 22 ], [ { id: 11, title: { rendered: 'Alpha' } } ] ) ).resolves.toEqual( [
|
|
35
|
+
{ value: 11, label: 'Alpha' },
|
|
36
|
+
{ value: 22, label: 'Unavailable content (22)' },
|
|
37
|
+
] );
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
// A label is matched back to its ID, so two unavailable items sharing one
|
|
41
|
+
// would collapse onto a single ID.
|
|
42
|
+
it( 'tells two unavailable IDs apart', async () => {
|
|
43
|
+
const posts = await fetchSavedPosts( [ 22, 33 ], [] );
|
|
44
|
+
expect( posts.map( post => post.label ) ).toEqual( [ 'Unavailable content (22)', 'Unavailable content (33)' ] );
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
it( 'still marks the content unavailable when the title is empty', async () => {
|
|
48
|
+
const posts = await fetchSavedPosts( [ 11, 22 ], [ { id: 11, title: { rendered: '' } } ] );
|
|
49
|
+
expect( posts.map( post => post.label ) ).toEqual( [ '(no title)', 'Unavailable content (22)' ] );
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
it( 'asks for nothing when nothing is saved', async () => {
|
|
53
|
+
await expect( fetchSavedPosts( [], [] ) ).resolves.toEqual( [] );
|
|
54
|
+
expect( apiFetch ).not.toHaveBeenCalled();
|
|
55
|
+
} );
|
|
56
|
+
} );
|