Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experience Blocks #24

Merged
merged 44 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7507d69
First draft experience blocks
roborourke May 11, 2020
ad4bf72
add eslint and fix code style issues
roborourke May 12, 2020
bc20304
record an event when an XB is viewed
roborourke May 12, 2020
beea777
Fix nodelist iterator
roborourke May 12, 2020
07dc288
Web components cannot be self closing
roborourke May 12, 2020
7dc8b51
Fix positioning of the audience picker component
roborourke May 12, 2020
de274dc
Test load experiments.js async
roborourke May 13, 2020
214d516
remove URLInput in favour of UI from main analytics plugin
roborourke May 14, 2020
8965c19
Adding UI polish plus copy function
roborourke May 16, 2020
2ea9fb5
fix white space error
roborourke May 16, 2020
6110a33
switch to using webpack manifest plugin
roborourke May 18, 2020
f5e6834
Fix initial XB insert logic, innerBlocks template wasnt working
roborourke May 18, 2020
bb0b6e5
Support for using audience names or fallback for variant
roborourke May 18, 2020
5589659
fix linter errors
roborourke May 18, 2020
19ee037
Dont uppercase the XB header
roborourke May 18, 2020
67c2db3
Ensure custom elements are only defined when analytics.js has loaded
roborourke May 18, 2020
0fe1fa2
expose `Test.registerGoal` before defining custom elements
roborourke May 18, 2020
80b8b23
Ensure BuildURL is defined for file chunks
roborourke May 18, 2020
73b2a7f
Add missing docblocks and fix use statements
roborourke May 20, 2020
5366588
rename block to personalized content and add XB block category
roborourke May 22, 2020
d75ce8f
Improve personalised content block header styling for dark backgrounds
roborourke May 22, 2020
624fbac
Bind setContent to the current instance
rmccue May 27, 2020
3905c75
Merge pull request #27 from humanmade/bind-setcontent
roborourke May 27, 2020
bedb416
Use explicit fallback block that cannot be removed
roborourke May 28, 2020
d503042
compare `json_last_error()` to `JSON_ERROR_NONE` constant
roborourke May 29, 2020
371ccf8
Pre fetch posts and only re-render XB when isLoading changes
roborourke May 29, 2020
f04b669
Merge branch 'experience-blocks' of github.com:humanmade/experiments …
roborourke May 29, 2020
a38ed58
Use component for rendering variant title
roborourke May 29, 2020
588d445
Code review improvements
roborourke Jun 1, 2020
5e20d29
Commit new component files
roborourke Jun 1, 2020
f6db732
Check if audience is set on client side rather than fallback var
roborourke Jun 1, 2020
a7f54ae
Add missing semi colons
roborourke Jun 1, 2020
50b5d2f
Fix operator spacing
roborourke Jun 1, 2020
ea18161
Check for error on audience object in VariantTitle
roborourke Jun 1, 2020
4d038a3
Use plugin file constant for `plugins_url()` calls
roborourke Jun 1, 2020
d421491
Check audience status for `trash` as well API error
roborourke Jun 1, 2020
73b10f5
Rename callback to be prefixed with `on*`
roborourke Jun 3, 2020
bc7e45e
Fix styling for WP 5.4
roborourke Jun 3, 2020
f064ad8
Fix flexbox toolbar issues
roborourke Jun 3, 2020
218e43c
Merge master
roborourke Jun 4, 2020
d1514b0
Fix minor coding standards pieces
rmccue Jun 4, 2020
ac81eca
Simplify some tidbits
rmccue Jun 4, 2020
ddc9dd0
Correct a misspellling
rmccue Jun 4, 2020
e59f4fc
Use ROOT_FILE for title ab test asset URL
roborourke Jun 5, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
rmccue marked this conversation as resolved.
Show resolved Hide resolved
}
}
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() {
roborourke marked this conversation as resolved.
Show resolved Hide resolved
require_once __DIR__ . '/personalization/register.php';
require_once __DIR__ . '/personalization-variant/register.php';
Comment on lines +14 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should ideally be in the main plugin.php; if this is a pattern already in the other "features", that's OK, but we should switch in a future PR.


// 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 {
roborourke marked this conversation as resolved.
Show resolved Hide resolved
roborourke marked this conversation as resolved.
Show resolved Hide resolved
$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 ) {
roborourke marked this conversation as resolved.
Show resolved Hide resolved
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' );
rmccue marked this conversation as resolved.
Show resolved Hide resolved
}

// 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