Skip to content

Commit

Permalink
Merge pull request #3849 from Automattic/add/enrolled-content-block
Browse files Browse the repository at this point in the history
Add restricted content blocks
  • Loading branch information
gikaragia authored Jan 21, 2021
2 parents d8af529 + 7af05bc commit 17a321d
Show file tree
Hide file tree
Showing 17 changed files with 415 additions and 39 deletions.
14 changes: 14 additions & 0 deletions assets/blocks/restricted-content/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "sensei-lms/restricted-content",
"category": "sensei-lms",
"supports": {
"html": false,
"align": [ "wide", "full" ]
},
"attributes": {
"restrictionType": {
"type": "string",
"default": "enrolled"
}
}
}
65 changes: 65 additions & 0 deletions assets/blocks/restricted-content/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { InnerBlocks } from '@wordpress/block-editor';
import { compose } from '@wordpress/compose';
import { withSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { RestrictedContentSettings } from './settings';
import classnames from 'classnames';

export const RestrictOptions = {
ENROLLED: 'enrolled',
UNENROLLED: 'unenrolled',
COURSE_COMPLETED: 'course-completed',
};

export const RestrictOptionLabels = {
[ RestrictOptions.ENROLLED ]: __( 'Enrolled Users', 'sensei-lms' ),
[ RestrictOptions.UNENROLLED ]: __( 'Unenrolled Users', 'sensei-lms' ),
[ RestrictOptions.COURSE_COMPLETED ]: __(
'Course Completed',
'sensei-lms'
),
};

const EditRestrictedContent = ( {
className,
hasInnerBlocks,
clientId,
attributes: { restrictionType },
setAttributes,
} ) => {
return (
<>
<div className={ classnames( 'wp-block-group', className ) }>
<div className="wp-block-group__inner-container">
<InnerBlocks
renderAppender={
! hasInnerBlocks && InnerBlocks.ButtonBlockAppender
}
/>
</div>
</div>
<RestrictedContentSettings
selectedRestriction={ restrictionType }
onRestrictionChange={ ( option ) =>
setAttributes( {
restrictionType: option,
} )
}
clientId={ clientId }
hasInnerBlocks={ hasInnerBlocks }
/>
</>
);
};

export default compose( [
withSelect( ( select, { clientId } ) => {
const { getBlock } = select( 'core/block-editor' );

const block = getBlock( clientId );

return {
hasInnerBlocks: !! ( block && block.innerBlocks.length ),
};
} ),
] )( EditRestrictedContent );
75 changes: 75 additions & 0 deletions assets/blocks/restricted-content/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { __ } from '@wordpress/i18n';
import { Icon } from '@wordpress/components';
import edit from './edit';
import save from './save';
import metadata from './block';
import { createBlock } from '@wordpress/blocks';

export default {
title: __( 'Restricted Course Content', 'sensei-lms' ),
description: __(
'Content inside this container block can be restricted to a specified group of users. Possible options are enrolled users, not enrolled users and users who have completed the course',
'sensei-lms'
),
keywords: [
__( 'Enrolled', 'sensei-lms' ),
__( 'Content', 'sensei-lms' ),
__( 'Locked', 'sensei-lms' ),
__( 'Private', 'sensei-lms' ),
__( 'Completed', 'sensei-lms' ),
__( 'Unenrolled', 'sensei-lms' ),
__( 'Restricted', 'sensei-lms' ),
],
icon: () => <Icon icon="lock" />,
edit,
save,
...metadata,
transforms: {
from: [
{
type: 'block',
isMultiBlock: true,
blocks: [ '*' ],
__experimentalConvert: ( blocks ) => {
if (
blocks.length === 1 &&
blocks[ 0 ].name === 'sensei-lms/restricted-content'
) {
return;
}

// The conversion is done by creating a wrapper block and setting the selected blocks as inner blocks.
const wrapperInnerBlocks = blocks.map( ( block ) => {
return createBlock(
block.name,
block.attributes,
block.innerBlocks
);
} );

const alignments = [ 'wide', 'full' ];

// Determine the widest setting of all the blocks to be grouped.
const widestAlignment = blocks.reduce(
( result, block ) => {
const { align } = block.attributes;
return alignments.indexOf( align ) >
alignments.indexOf( result )
? align
: result;
},
undefined
);

return createBlock(
'sensei-lms/restricted-content',
{
align: widestAlignment,
},
wrapperInnerBlocks
);
},
},
],
},
};
12 changes: 12 additions & 0 deletions assets/blocks/restricted-content/save.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { InnerBlocks } from '@wordpress/block-editor';
import classnames from 'classnames';

export default function SaveRestrictedContent( { className } ) {
return (
<div className={ classnames( 'wp-block-group', className ) }>
<div className="wp-block-group__inner-container">
<InnerBlocks.Content />
</div>
</div>
);
}
106 changes: 106 additions & 0 deletions assets/blocks/restricted-content/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
BlockControls,
BlockSettingsMenuControls,
} from '@wordpress/block-editor';
import { ToolbarGroup, MenuItem } from '@wordpress/components';
import { RestrictOptions, RestrictOptionLabels } from './edit';
import { __ } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
import ToolbarDropdown from '../editor-components/toolbar-dropdown';

/**
* Check if the current block is the only one that is selected.
*
* @param {string} clientId The block client id.
*/
const useIsSingleRestrictSelected = ( clientId ) => {
return useSelect(
( select ) => {
const selectedClientIds = select(
'core/block-editor'
).getSelectedBlockClientIds();

return (
selectedClientIds.length === 1 &&
selectedClientIds[ 0 ] === clientId
);
},
[ clientId ]
);
};

/**
* A hook that returns a function which unwraps the inner blocks from the restricted content block.
*
* @param {string} clientId The block client id.
*/
const useOnRestrictionRemoval = ( clientId ) => {
const block = useSelect(
( select ) => select( 'core/block-editor' ).getBlock( clientId ),
[ clientId ]
);
const { replaceBlocks } = useDispatch( 'core/block-editor' );

return () => {
if ( block.innerBlocks.length ) {
replaceBlocks( clientId, block.innerBlocks );
}
};
};

/**
* The restricted content block settings.
*
* @param {Object} props Component properties.
* @param {string} props.selectedRestriction The restriction that is currently selected.
* @param {Function} props.onRestrictionChange Callback which is called when a new option is selected.
* @param {string} props.clientId The block client id.
* @param {boolean} props.hasInnerBlocks True if there are inner blocks.
*/
export function RestrictedContentSettings( {
selectedRestriction,
onRestrictionChange,
clientId,
hasInnerBlocks,
} ) {
const isSingleRestrictSelected = useIsSingleRestrictSelected( clientId );
const onRestrictRemoval = useOnRestrictionRemoval( clientId );

const toolbarOptions = Object.keys( RestrictOptions ).map(
( optionKey ) => ( {
value: RestrictOptions[ optionKey ],
label: RestrictOptionLabels[ RestrictOptions[ optionKey ] ],
} )
);

return (
<>
<BlockControls>
<ToolbarGroup>
<ToolbarDropdown
options={ toolbarOptions }
optionsLabel={ __( 'Visibility', 'sensei-lms' ) }
value={ selectedRestriction }
onChange={ onRestrictionChange }
/>
</ToolbarGroup>
</BlockControls>
{ isSingleRestrictSelected &&
hasInnerBlocks &&
BlockSettingsMenuControls && (
<BlockSettingsMenuControls>
{ ( { onClose } ) => (
<MenuItem
onClick={ () => {
onRestrictRemoval();
onClose();
} }
>
{ __( 'Remove restriction', 'sensei-lms' ) }
</MenuItem>
) }
</BlockSettingsMenuControls>
) }
</>
);
}
2 changes: 2 additions & 0 deletions assets/blocks/sensei-single-course-blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import registerSenseiBlocks from './register-sensei-blocks';
import TakeCourseButtonBlock from './take-course';
import ContactTeacherButton from './contact-teacher';
import CourseProgressBlock from './course-progress';
import RestrictedContent from './restricted-content';
import {
CourseOutlineBlock,
CourseOutlineLessonBlock,
Expand All @@ -15,4 +16,5 @@ registerSenseiBlocks( [
TakeCourseButtonBlock,
ContactTeacherButton,
CourseProgressBlock,
RestrictedContent,
] );
2 changes: 1 addition & 1 deletion includes/blocks/class-sensei-block-contact-teacher.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Sensei_Block_Contact_Teacher {
* Sensei_Block_Contact_Teacher constructor.
*/
public function __construct() {
add_action( 'init', [ $this, 'register_block' ] );
$this->register_block();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion includes/blocks/class-sensei-block-take-course.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Sensei_Block_Take_Course {
* Sensei_Block_Take_Course constructor.
*/
public function __construct() {
add_action( 'init', [ $this, 'register_block' ] );
$this->register_block();
}


Expand Down
39 changes: 37 additions & 2 deletions includes/blocks/class-sensei-course-blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,53 @@ class Sensei_Course_Blocks {
public $take_course;

/**
* Sensei_Blocks constructor.
* Sensei_Course_Blocks constructor.
*/
public function __construct() {
add_action( 'enqueue_block_assets', [ $this, 'enqueue_block_assets' ] );
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_assets' ] );
add_filter( 'sensei_use_sensei_template', [ 'Sensei_Course_Blocks', 'skip_single_course_template' ] );
add_action( 'template_redirect', [ $this, 'maybe_initialize_blocks' ] );
add_action( 'current_screen', [ $this, 'maybe_initialize_blocks' ] );
}

/**
* Check if course blocks should be initialized and do initialization.
*
* @access private
*/
public function maybe_initialize_blocks() {
if ( is_admin() ) {
$screen = get_current_screen();

// Init blocks.
if ( ! $screen->is_block_editor || 'course' !== $screen->post_type ) {
return;
}
} elseif ( 'course' !== get_post_type() ) {
return;
}

$this->initialize_blocks();
}

/**
* Initialize blocks that are used in course pages.
*/
public function initialize_blocks() {
$this->outline = new Sensei_Course_Outline_Block();
$this->progress = new Sensei_Course_Progress_Block();
$this->contact_teacher = new Sensei_Block_Contact_Teacher();
$this->take_course = new Sensei_Block_Take_Course();
new Sensei_Restricted_Content_Block();

$post_type_object = get_post_type_object( 'course' );

$post_type_object->template = [
[ 'sensei-lms/button-take-course' ],
[ 'sensei-lms/button-contact-teacher' ],
[ 'sensei-lms/course-progress' ],
[ 'sensei-lms/course-outline' ],
];
}

/**
Expand Down
Loading

0 comments on commit 17a321d

Please sign in to comment.