Skip to content

Commit

Permalink
Merge pull request #24 from humanmade/experience-blocks
Browse files Browse the repository at this point in the history
Experience Blocks
  • Loading branch information
roborourke authored Jun 5, 2020
2 parents fa18a9b + e59f4fc commit c9e12b1
Show file tree
Hide file tree
Showing 38 changed files with 4,159 additions and 1,692 deletions.
12 changes: 12 additions & 0 deletions .eslintrc
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"
}
}
71 changes: 71 additions & 0 deletions inc/features/blocks/namespace.php
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;
}
25 changes: 25 additions & 0 deletions inc/features/blocks/personalization-variant/block.json
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"
}
}
}
}
53 changes: 53 additions & 0 deletions inc/features/blocks/personalization-variant/edit.js
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 );
18 changes: 18 additions & 0 deletions inc/features/blocks/personalization-variant/index.js
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 );
95 changes: 95 additions & 0 deletions inc/features/blocks/personalization-variant/register.php
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
);
}
11 changes: 11 additions & 0 deletions inc/features/blocks/personalization-variant/save.js
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;
17 changes: 17 additions & 0 deletions inc/features/blocks/personalization/block.json
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 inc/features/blocks/personalization/components/variant-panel.js
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 inc/features/blocks/personalization/components/variant-title.js
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;
Loading

0 comments on commit c9e12b1

Please sign in to comment.