-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24 from humanmade/experience-blocks
Experience Blocks
- Loading branch information
Showing
38 changed files
with
4,159 additions
and
1,692 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"extends": "humanmade", | ||
"globals": { | ||
"Altis": "readonly", | ||
"wp": "readonly", | ||
"moment": "readonly" | ||
}, | ||
"rules": { | ||
"no-multi-str": "off", | ||
"no-console": "off" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?php | ||
/** | ||
* Experience Block functions. | ||
* | ||
* @package altis-experiments | ||
*/ | ||
|
||
namespace Altis\Experiments\Features\Blocks; | ||
|
||
/** | ||
* Include and set up Experience Blocks. | ||
*/ | ||
function setup() { | ||
require_once __DIR__ . '/personalization/register.php'; | ||
require_once __DIR__ . '/personalization-variant/register.php'; | ||
|
||
// Register blocks. | ||
Personalization\setup(); | ||
Personalization_Variant\setup(); | ||
|
||
// Register experience block category. | ||
add_filter( 'block_categories', __NAMESPACE__ . '\\add_block_category', 9 ); | ||
} | ||
|
||
/** | ||
* Adds an experience block category to the block editor. | ||
* | ||
* @param array $categories Array of block editor block type categories. | ||
* @return array The modified block categories array. | ||
*/ | ||
function add_block_category( array $categories ) : array { | ||
$categories[] = [ | ||
'slug' => 'altis-experience-blocks', | ||
'title' => __( 'Experience Blocks', 'altis-experiments' ), | ||
]; | ||
|
||
return $categories; | ||
} | ||
|
||
/** | ||
* Reads and returns a block.json file to pass shared settings | ||
* between JS and PHP to the register blocks functions. | ||
* | ||
* @param string $name The directory name of the block relative to this file. | ||
* @return array|null The JSON data as an associative array or null on error. | ||
*/ | ||
function get_block_settings( string $name ) : ?array { | ||
$json_path = __DIR__ . '/' . $name . '/block.json'; | ||
|
||
// Check name is valid. | ||
if ( ! file_exists( $json_path ) ) { | ||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | ||
trigger_error( sprintf( 'Error reading %/block.json: file does not exist.', $name ), E_USER_WARNING ); | ||
return null; | ||
} | ||
|
||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents | ||
$json = file_get_contents( $json_path ); | ||
|
||
// Decode the settings. | ||
$settings = json_decode( $json, ARRAY_A ); | ||
|
||
// Check JSON is valid. | ||
if ( json_last_error() !== JSON_ERROR_NONE ) { | ||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | ||
trigger_error( sprintf( 'Error decoding %/block.json: %s', $name, json_last_error_msg() ), E_USER_WARNING ); | ||
return null; | ||
} | ||
|
||
return $settings; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"name": "altis/personalization-variant", | ||
"settings": { | ||
"category": "altis-experience-blocks", | ||
"icon": "groups", | ||
"parent": [ "altis/personalization" ], | ||
"supports": { | ||
"reusable": false, | ||
"html": false, | ||
"lightBlockWrapper": true, | ||
"inserter": false | ||
}, | ||
"attributes": { | ||
"parentId": { | ||
"type": "string" | ||
}, | ||
"audience": { | ||
"type": "number" | ||
}, | ||
"fallback": { | ||
"type": "boolean" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import React, { useEffect } from 'react'; | ||
|
||
const { InnerBlocks } = wp.blockEditor; | ||
const { compose } = wp.compose; | ||
const { withSelect, withDispatch } = wp.data; | ||
|
||
const Edit = ( { | ||
hasChildBlocks, | ||
isSelected, | ||
onSelect, | ||
} ) => { | ||
// Select the block parent if a variant is directly selected. | ||
useEffect( () => { | ||
if ( isSelected ) { | ||
onSelect(); | ||
} | ||
}, [ isSelected ] ); | ||
|
||
const props = {}; | ||
if ( ! hasChildBlocks ) { | ||
// If we don't have any child blocks, show large block appender button. | ||
props.renderAppender = () => <InnerBlocks.ButtonBlockAppender />; | ||
} | ||
|
||
return ( | ||
<InnerBlocks | ||
{ ...props } | ||
/> | ||
); | ||
}; | ||
|
||
export default compose( | ||
withSelect( ( select, ownProps ) => { | ||
const { clientId } = ownProps; | ||
const { getBlockOrder } = select( 'core/block-editor' ); | ||
|
||
return { | ||
hasChildBlocks: () => getBlockOrder( clientId ).length > 0, | ||
}; | ||
} ), | ||
withDispatch( ( dispatch, ownProps, registry ) => { | ||
const { clientId } = ownProps; | ||
const { getBlockRootClientId } = registry.select( 'core/block-editor' ); | ||
const { selectBlock } = dispatch( 'core/block-editor' ); | ||
|
||
// Get parent block client ID. | ||
const rootClientId = getBlockRootClientId( clientId ); | ||
|
||
return { | ||
onSelect: () => selectBlock( rootClientId ), | ||
}; | ||
} ), | ||
)( Edit ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import edit from './edit'; | ||
import save from './save'; | ||
|
||
import blockData from './block.json'; | ||
|
||
const { registerBlockType } = wp.blocks; | ||
const { __ } = wp.i18n; | ||
|
||
const settings = { | ||
title: __( 'Personalized Content Variant', 'altis-experiments' ), | ||
description: __( 'Personalized content block items', 'altis-experiments' ), | ||
edit, | ||
save, | ||
...blockData.settings, | ||
}; | ||
|
||
// Register block. | ||
registerBlockType( blockData.name, settings ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
<?php | ||
/** | ||
* Personalization Variant Block Server Side. | ||
* | ||
* @phpcs:disable HM.Files.NamespaceDirectoryName.NameMismatch | ||
* @phpcs:disable HM.Files.FunctionFileName.WrongFile | ||
* | ||
* @package altis-experiments | ||
*/ | ||
|
||
namespace Altis\Experiments\Features\Blocks\Personalization_Variant; | ||
|
||
use Altis\Experiments; | ||
use Altis\Experiments\Features\Blocks; | ||
use Altis\Experiments\Utils; | ||
|
||
const BLOCK = 'personalization-variant'; | ||
|
||
/** | ||
* Registers the block type assets and server side rendering. | ||
*/ | ||
function setup() { | ||
$block_data = Blocks\get_block_settings( BLOCK ); | ||
|
||
// Queue up JS files. | ||
add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\\enqueue_assets' ); | ||
|
||
// Register the block. | ||
register_block_type( $block_data['name'], [ | ||
'attributes' => $block_data['settings']['attributes'], | ||
'render_callback' => __NAMESPACE__ . '\\render_block', | ||
] ); | ||
} | ||
|
||
/** | ||
* Enqueues the block assets. | ||
*/ | ||
function enqueue_assets() { | ||
wp_enqueue_script( | ||
'altis-experiments-features-blocks-personalization-variant', | ||
Utils\get_asset_url( 'features/blocks/personalization-variant.js' ), | ||
[], | ||
null | ||
); | ||
|
||
wp_add_inline_script( | ||
'altis-experiments-features-blocks-personalization-variant', | ||
sprintf( | ||
'window.Altis = window.Altis || {};' . | ||
'window.Altis.Analytics = window.Altis.Analytics || {};' . | ||
'window.Altis.Analytics.Experiments = window.Altis.Analytics.Experiments || {};' . | ||
'window.Altis.Analytics.Experiments.BuildURL = %s;', | ||
wp_json_encode( plugins_url( 'build', Experiments\ROOT_FILE ) ) | ||
), | ||
'before' | ||
); | ||
} | ||
|
||
/** | ||
* Render callback for the personalization variant block. | ||
* | ||
* Because this block only saves <InnerBlocks.Content> on the JS side, | ||
* the content string represents only the wrapped inner block markup. | ||
* | ||
* @param array $attributes The block's attributes object. | ||
* @param string $innerContent The block's saved content. | ||
* @return string The final rendered block markup, as an HTML string. | ||
*/ | ||
function render_block( array $attributes, ?string $inner_content = '' ) : string { | ||
$parent_id = $attributes['parentId'] ?? false; | ||
$audience = $attributes['audience'] ?? 0; | ||
$fallback = $attributes['fallback'] ?? false; | ||
|
||
if ( ! $parent_id ) { | ||
trigger_error( 'Personalization block variant has no parent ID set.', E_USER_WARNING ); | ||
return ''; | ||
} | ||
|
||
// If this is the fallback variant output the template with different attributes | ||
// for easier and more specific targeting by document.querySelector(). | ||
if ( $fallback ) { | ||
return sprintf( | ||
'<template data-fallback data-parent-id="%s">%s</template>', | ||
esc_attr( $parent_id ), | ||
$inner_content | ||
); | ||
} | ||
|
||
return sprintf( | ||
'<template data-audience="%d" data-parent-id="%s">%s</template>', | ||
esc_attr( $audience ), | ||
esc_attr( $parent_id ), | ||
$inner_content | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React from 'react'; | ||
|
||
const { InnerBlocks } = wp.blockEditor; | ||
|
||
const Save = () => { | ||
return ( | ||
<InnerBlocks.Content /> | ||
); | ||
}; | ||
|
||
export default Save; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "altis/personalization", | ||
"settings": { | ||
"category": "altis-experience-blocks", | ||
"icon": "groups", | ||
"supports": { | ||
"alignWide": true, | ||
"html": false, | ||
"align": true | ||
}, | ||
"attributes": { | ||
"clientId": { | ||
"type": "string" | ||
} | ||
} | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
inc/features/blocks/personalization/components/variant-panel.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import React from 'react'; | ||
import VariantTitle from './variant-title'; | ||
|
||
const { AudiencePicker } = Altis.Analytics.components; | ||
|
||
const { PanelBody } = wp.components; | ||
const { useDispatch } = wp.data; | ||
const { __ } = wp.i18n; | ||
|
||
const VariantPanel = ( { variant } ) => { | ||
const { updateBlockAttributes } = useDispatch( 'core/block-editor' ); | ||
|
||
if ( variant.attributes.fallback ) { | ||
return ( | ||
<PanelBody title={ __( 'Fallback', 'altis-experiments' ) }> | ||
<p className="description"> | ||
{ __( 'This variant will be shown as a fallback if no audiences are matched. You can leave the content empty if you do not wish to show anything.', 'altis-experiments' ) } | ||
</p> | ||
</PanelBody> | ||
); | ||
} | ||
|
||
return ( | ||
<PanelBody title={ <VariantTitle variant={ variant } /> }> | ||
<AudiencePicker | ||
label={ __( 'Audience' ) } | ||
audience={ variant.attributes.audience } | ||
onSelect={ audience => updateBlockAttributes( variant.clientId, { audience: audience.id } ) } | ||
onClearSelection={ () => updateBlockAttributes( variant.clientId, { audience: null } ) } | ||
/> | ||
{ ! variant.attributes.audience && ( | ||
<p className="description"> | ||
{ __( 'You must select an audience for this variant.', 'altis-experiments' ) } | ||
</p> | ||
) } | ||
</PanelBody> | ||
); | ||
}; | ||
|
||
export default VariantPanel; |
45 changes: 45 additions & 0 deletions
45
inc/features/blocks/personalization/components/variant-title.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
const { useSelect } = wp.data; | ||
const { __ } = wp.i18n; | ||
|
||
// Component for fetching and displaying the variant title string. | ||
const VariantTitle = ( { variant } ) => { | ||
const audience = useSelect( select => { | ||
return select( 'audience' ).getPost( variant.attributes.audience ); | ||
}, [ variant.attributes.audience ] ); | ||
|
||
const isLoading = useSelect( select => select( 'audience' ).getIsLoading(), [] ); | ||
|
||
if ( variant.attributes.fallback ) { | ||
return __( 'Fallback', 'altis-experiments' ); | ||
} | ||
|
||
if ( ! variant.attributes.audience ) { | ||
return __( 'Select audience', 'altis-experiments' ); | ||
} | ||
|
||
const status = ( audience && audience.status ) || 'draft'; | ||
const title = audience && audience.title && audience.title.rendered; | ||
|
||
// Audience is valid and has a title. | ||
if ( status !== 'trash' && title ) { | ||
return audience.title.rendered; | ||
} | ||
|
||
// Audience has been deleted. | ||
if ( status === 'trash' ) { | ||
return __( '(deleted)', 'altis-experiments' ); | ||
} | ||
|
||
// Check if audience reponse is a REST API error. | ||
if ( audience && audience.error && audience.error.message ) { | ||
return audience.error.message; | ||
} | ||
|
||
if ( isLoading ) { | ||
return __( 'Loading...', 'altis-experiments' ); | ||
} | ||
|
||
return ''; | ||
}; | ||
|
||
export default VariantTitle; |
Oops, something went wrong.