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

Show notice in Course Editor to focus on Course Outline block or Insert one if block does not exist for first time users #7364

Open
wants to merge 30 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9d77b03
Show notice to create outline, or focus outline first time
Imran92 Dec 6, 2023
b556656
Add import comments
Imran92 Dec 6, 2023
d328e85
Add notice file in webpack
Imran92 Dec 6, 2023
4dcfa9d
Enqueue first course notice only when first course
Imran92 Dec 6, 2023
8325305
Test published lesson checking function
Imran92 Dec 6, 2023
bfdcdb5
Add tests for conditionally creating or selecting outline block
Imran92 Dec 6, 2023
b1b0d13
Update logic to show notice
Imran92 Dec 6, 2023
324e6d2
Add changelog
Imran92 Dec 6, 2023
af681c6
Update the comment
Imran92 Dec 6, 2023
ddb29ea
Only show notice after sensei pattern inserted
Imran92 Dec 12, 2023
f424669
Show notice only when no course of any status exists
Imran92 Dec 12, 2023
d840ca2
Merge branch 'trunk' into add/notice-to-create-course-outline-block-f…
Imran92 Dec 12, 2023
88ee206
Merge branches 'add/notice-to-create-course-outline-block-first-time'…
Imran92 Dec 12, 2023
12bd25d
Move pattern variable check inside function
Imran92 Dec 12, 2023
4bc987c
Show notice when there is no published lesson
Imran92 Dec 12, 2023
7460bd6
Fix test
Imran92 Dec 12, 2023
1cfec71
Merge branch 'trunk' into add/notice-to-create-course-outline-block-f…
donnapep Dec 14, 2023
1a7aba8
Merge branch 'trunk' into add/notice-to-create-course-outline-block-f…
Imran92 Dec 15, 2023
c43f79c
Fix failing tests
Imran92 Dec 15, 2023
e84c62a
Show notice only when no lessons are created
Imran92 Dec 15, 2023
f749ad7
Wait for pattern on new course, else show right away
Imran92 Dec 15, 2023
3d82cd5
change name of hasOutlineBlock function
Imran92 Jan 1, 2024
05099e6
Add meta for new post for course
Imran92 Jan 1, 2024
ee75e87
Remove inserter pattern selection detection code
Imran92 Jan 1, 2024
a633f08
Manually select outline block only when available
Imran92 Jan 1, 2024
13e2d98
Remove unused import
Imran92 Jan 1, 2024
1166860
Merge branch 'trunk' into add/notice-to-create-course-outline-block-f…
Imran92 Jan 1, 2024
3d47a01
Remove unnecessary async
Imran92 Jan 5, 2024
5e5e764
Set default value to null
Imran92 Jan 5, 2024
ad3aeee
Set default to false
Imran92 Jan 5, 2024
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
2 changes: 1 addition & 1 deletion assets/admin/editor-wizard/steps/patterns-step.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
? replacePlaceholders( blocks, replaces )
: blocks;

resetEditorBlocks( newBlocks );
resetEditorBlocks( newBlocks, { patternName: name } );

Check warning on line 40 in assets/admin/editor-wizard/steps/patterns-step.js

View check run for this annotation

Codecov / codecov/patch

assets/admin/editor-wizard/steps/patterns-step.js#L40

Added line #L40 was not covered by tests
onCompletion();

//Auto select a template if the pattern specifies an available one.
Expand Down
125 changes: 125 additions & 0 deletions assets/js/admin/first-course-creation-notice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks';
import { select, subscribe, dispatch } from '@wordpress/data';
import domReady from '@wordpress/dom-ready';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getFirstBlockByName } from '../../blocks/course-outline/data';

export const getOutlineBlock = () =>
getFirstBlockByName(
'sensei-lms/course-outline',
select( 'core/block-editor' ).getBlocks()
);

export const handleCourseOutlineBlockIncomplete = () => {
const courseOutlineBlock = getOutlineBlock();

// If the course outline block exists, just select it.
if ( courseOutlineBlock ) {
dispatch( 'core/editor' ).selectBlock( courseOutlineBlock.clientId );
return;
}

const { insertBlock } = dispatch( 'core/block-editor' );

insertBlock( createBlock( 'sensei-lms/course-outline' ) );
};

// If the function isn't globally available, the link button doesn't find the reference.
window.handleCourseOutlineBlockIncomplete = handleCourseOutlineBlockIncomplete;

export const hasLessonInOutline = ( blocks ) => {
return blocks.some( ( block ) => {
if ( block.name === 'sensei-lms/course-outline-lesson' ) {
return true;
}

if ( block.innerBlocks?.length ) {
return hasLessonInOutline( block.innerBlocks );

Check warning on line 43 in assets/js/admin/first-course-creation-notice.js

View check run for this annotation

Codecov / codecov/patch

assets/js/admin/first-course-creation-notice.js#L43

Added line #L43 was not covered by tests
}

return false;
} );
};

export const handleFirstCourseCreationHelperNotice = () => {
const { createInfoNotice, removeNotice } = dispatch( 'core/notices' );
const userId = select( 'core' ).getCurrentUser()?.id;
const { getEditedPostAttribute } = select( 'core/editor' );
const { editPost } = dispatch( 'core/editor' );
const firstCourseNoticeDismissedKey =
'sensei-lms-first-course-notice-dismissed-' + userId;
const isFirstCourseNoticeDismissed = !! window.localStorage.getItem(
firstCourseNoticeDismissedKey
);
const noticeId = 'course-outline-block-setup-incomplete';
const isNewCourse = window?.sensei?.isNewCourse;

let noticeCreated = false;
let noticeRemoved = false;

const notice = __(
'Nice! Now you can <a href="javascript:;" onclick="window?.handleCourseOutlineBlockIncomplete();">add some lessons</a> to your course.',
Copy link
Contributor

Choose a reason for hiding this comment

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

My suggestion wouldn't match the design, but it would match the default behavior of the Gutenberg notices.

Should we tweak it to use a separate button, being the actions from the Gutenberg notice instead? cc @donnapep

So we wouldn't need to put the function in the window, we wouldn't need to add the onclick and href="javascript:;" as HTML attributes, it would get more organized in terms of code, and the users would the same behavior of other notices.

If we decide to not change it, we need to extract the HTML and logic from the translation string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we're good with the design change, we can go for it 👍 Our current design of showing it as a URL looks better to me. But as showing it as button is simpler, as that's the default behavior. I think the main benefit of this will be not having to use the __unstableHTML property.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm fine with changing it to use a button if it helps simplify things. In that case, we could change the message to:

Nice! Now you can add some content to your course. Add Lessons

where Add Lessons is a button that jumps to / adds the Course Outline block.

Let's see what that looks like. 🙂

Copy link
Collaborator

Choose a reason for hiding this comment

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

Updated comment here 🙂

'sensei-lms'
);

let isNewPostMetaSet = false;

subscribe( () => {
if ( isNewCourse && ! isNewPostMetaSet ) {
if ( getOutlineBlock() ) {
isNewPostMetaSet = true;
editPost( {

Check warning on line 77 in assets/js/admin/first-course-creation-notice.js

View check run for this annotation

Codecov / codecov/patch

assets/js/admin/first-course-creation-notice.js#L76-L77

Added lines #L76 - L77 were not covered by tests
meta: { _new_post: true },
} );
}
}
const patternSelected =
! isNewCourse ||
( isNewPostMetaSet &&
false === getEditedPostAttribute( 'meta' )?._new_post );
if (
noticeCreated &&
! noticeRemoved &&
getOutlineBlock() &&
hasLessonInOutline( [ getOutlineBlock() ] )
) {
noticeRemoved = true;
noticeCreated = false;
removeNotice( noticeId );

Check warning on line 94 in assets/js/admin/first-course-creation-notice.js

View check run for this annotation

Codecov / codecov/patch

assets/js/admin/first-course-creation-notice.js#L92-L94

Added lines #L92 - L94 were not covered by tests
}

// If the user selects a Sensei pattern or editing an existing Course, and the notice hasn't been created, and notice hasn't been dismissed, and either the course outline block hasn't been created OR there are no published lessons in the outline, create the notice.
if (
patternSelected &&
! noticeCreated &&
! isFirstCourseNoticeDismissed &&
! (
getOutlineBlock() && hasLessonInOutline( [ getOutlineBlock() ] )
)
) {
noticeCreated = true;
noticeRemoved = false;

Check warning on line 107 in assets/js/admin/first-course-creation-notice.js

View check run for this annotation

Codecov / codecov/patch

assets/js/admin/first-course-creation-notice.js#L106-L107

Added lines #L106 - L107 were not covered by tests

createInfoNotice( notice, {

Check warning on line 109 in assets/js/admin/first-course-creation-notice.js

View check run for this annotation

Codecov / codecov/patch

assets/js/admin/first-course-creation-notice.js#L109

Added line #L109 was not covered by tests
id: noticeId,
isDismissible: true,
__unstableHTML: true, // Necessary to render the link in the middle of the notice message.
onDismiss: () => {
window.localStorage.setItem(

Check warning on line 114 in assets/js/admin/first-course-creation-notice.js

View check run for this annotation

Codecov / codecov/patch

assets/js/admin/first-course-creation-notice.js#L114

Added line #L114 was not covered by tests
firstCourseNoticeDismissedKey,
'1'
);
},
} );
}
} );
};

// Call function on dom ready.
domReady( handleFirstCourseCreationHelperNotice );
125 changes: 125 additions & 0 deletions assets/js/admin/first-course-creation-notice.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks';
import { dispatch, select, subscribe } from '@wordpress/data';
/**
* Internal dependencies
*/
import {
getOutlineBlock,
handleCourseOutlineBlockIncomplete,
handleFirstCourseCreationHelperNotice,
hasLessonInOutline,
} from './first-course-creation-notice';
import { getFirstBlockByName } from '../../blocks/course-outline/data';

// Initial mocks.
jest.mock( '@wordpress/blocks', () => ( {
createBlock: jest.fn().mockImplementation( () => ( {
attributes: {},
clientId: 'new-block-id',
} ) ),
} ) );

jest.mock( '@wordpress/data', () => ( {
dispatch: jest.fn().mockImplementation( () => ( {
createInfoNotice: jest.fn(),
removeNotice: jest.fn(),
insertBlock: jest.fn(),
selectBlock: jest.fn(),
} ) ),
select: jest.fn().mockImplementation( () => ( {
getCurrentUser: jest.fn(),
getBlocks: jest.fn(),
} ) ),
subscribe: jest.fn(),
use: jest.fn(),
} ) );

jest.mock( '@wordpress/i18n', () => ( {
...jest.requireActual( '@wordpress/i18n' ),
__: jest.fn(),
} ) );

jest.mock( './first-course-creation-notice' );
jest.mock( '../../blocks/course-outline/data' );

describe( 'hasPublishedLessonInOutline', () => {
beforeEach( () => {
hasLessonInOutline.mockImplementation(
jest.requireActual( './first-course-creation-notice' )
.hasLessonInOutline
);
} );

it( 'should return true when there is a lesson in the outline', () => {
const blocks = [
{
name: 'sensei-lms/course-outline-lesson',
},
];

const result = hasLessonInOutline( blocks );

expect( result ).toBe( true );
} );

it( 'should return false when there is no lesson in the outline', () => {
const blocks = [ { name: 'some-other-block' } ];

const result = hasLessonInOutline( blocks );

expect( result ).toBe( false );
} );
} );

describe( 'handleCourseOutlineBlockIncomplete', () => {
beforeEach( () => {
handleCourseOutlineBlockIncomplete.mockImplementation(
jest.requireActual( './first-course-creation-notice' )
.handleCourseOutlineBlockIncomplete
);
getFirstBlockByName.mockClear();
getOutlineBlock.mockClear();
createBlock.mockClear();
} );
it( 'should create and insert a block when no course outline block exists', () => {
// Mock hasOutlineBlock to return falsy.
getFirstBlockByName.mockImplementation( () => null );
const mockInsertBlock = jest.fn();
dispatch.mockImplementation( () => ( {
insertBlock: mockInsertBlock,
selectBlock: jest.fn(),
} ) );

handleCourseOutlineBlockIncomplete();

// Ensure createBlock and insertBlock were called with the correct parameters.
expect( createBlock ).toHaveBeenCalledWith(
'sensei-lms/course-outline'
);
expect( mockInsertBlock ).toHaveBeenCalled();
} );

it( 'should focus on the existing course outline block when it exists', () => {
// Mock hasOutlineBlock to return a truthy value.
getFirstBlockByName.mockImplementation( () => ( {
clientId: 'existing-block-id',
} ) );
const mockInsertBlock = jest.fn();
const mockSelectBlock = jest.fn();
dispatch.mockImplementation( () => ( {
insertBlock: mockInsertBlock,
selectBlock: mockSelectBlock,
} ) );

handleCourseOutlineBlockIncomplete();

// Ensure selectBlock was called with the correct parameters.
expect( mockSelectBlock ).toHaveBeenCalledWith( 'existing-block-id' );
// Ensure createBlock and insertBlock were not called.
expect( createBlock ).not.toHaveBeenCalled();
expect( mockInsertBlock ).not.toHaveBeenCalled();
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Notice on Course editor for first course of a user guiding them to use Course Outline block properly
47 changes: 46 additions & 1 deletion includes/class-sensei-course.php
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,12 @@
/**
* Register and enqueue scripts and styles that are needed in the backend.
*
* @param string $hook_suffix The current admin page.
*
* @access private
* @since 2.1.0
*/
public function register_admin_scripts() {
public function register_admin_scripts( $hook_suffix ) {

Check warning on line 456 in includes/class-sensei-course.php

View check run for this annotation

Codecov / codecov/patch

includes/class-sensei-course.php#L456

Added line #L456 was not covered by tests
$screen = get_current_screen();
if ( ! $screen ) {
return;
Expand All @@ -474,6 +476,38 @@
sprintf( 'window.sensei = window.sensei || {}; window.sensei.courseSettingsSidebar = %s;', $settings_sidebar ),
'before'
);

$lessons_by_user_args = array(
'author' => get_current_user_id(),
'fields' => 'all',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Runs only once during Course editor loading.
array(
'key' => '_lesson_course',
'value' => 0,
'compare' => '>',
),
),
'posts_per_page' => 1,
'post_status' => 'any',
'post_type' => 'lesson',
);

Check warning on line 493 in includes/class-sensei-course.php

View check run for this annotation

Codecov / codecov/patch

includes/class-sensei-course.php#L480-L493

Added lines #L480 - L493 were not covered by tests

$user_lessons_query = new WP_Query( $lessons_by_user_args );
$has_lessons_with_courses = count( $user_lessons_query->posts ) > 0;

Check warning on line 496 in includes/class-sensei-course.php

View check run for this annotation

Codecov / codecov/patch

includes/class-sensei-course.php#L495-L496

Added lines #L495 - L496 were not covered by tests

if ( ! $has_lessons_with_courses ) {
$post_id = get_the_ID();
$new_post = $post_id && get_post_meta( $post_id, '_new_post', true );
$is_new_post = 'post-new.php' === $hook_suffix || $new_post;

Check warning on line 501 in includes/class-sensei-course.php

View check run for this annotation

Codecov / codecov/patch

includes/class-sensei-course.php#L498-L501

Added lines #L498 - L501 were not covered by tests

wp_add_inline_script(
'sensei-admin-course-edit',
sprintf( 'window.sensei = window.sensei || {}; window.sensei.isNewCourse = %s;', ( $is_new_post ? 'true' : 'false' ) ),
'before'
);

Check warning on line 507 in includes/class-sensei-course.php

View check run for this annotation

Codecov / codecov/patch

includes/class-sensei-course.php#L503-L507

Added lines #L503 - L507 were not covered by tests

Sensei()->assets->enqueue( 'sensei-admin-first-course-creation-notice', 'js/admin/first-course-creation-notice.js', [ 'sensei-admin-course-edit' ], true );

Check warning on line 509 in includes/class-sensei-course.php

View check run for this annotation

Codecov / codecov/patch

includes/class-sensei-course.php#L509

Added line #L509 was not covered by tests
}
}

if ( 'edit-course' === $screen->id ) {
Expand Down Expand Up @@ -694,6 +728,17 @@
'auth_callback' => [ $this, 'post_meta_auth_callback' ],
]
);
register_post_meta(
'course',
'_new_post',
[
'show_in_rest' => true,
'single' => true,
'type' => 'boolean',
'default' => false,
'auth_callback' => [ $this, 'post_meta_auth_callback' ],
]
);

Check warning on line 741 in includes/class-sensei-course.php

View check run for this annotation

Codecov / codecov/patch

includes/class-sensei-course.php#L731-L741

Added lines #L731 - L741 were not covered by tests
/**
* Sets up the meta fields saved on course save in WP admin.
*
Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const files = [
'js/admin/course-edit.js',
'js/admin/course-index.js',
'js/admin/event-logging.js',
'js/admin/first-course-creation-notice.js',
'js/admin/lesson-bulk-edit.js',
'js/admin/lesson-quick-edit.js',
'js/admin/message-menu-fix.js',
Expand Down
Loading