diff --git a/.changelogs/course-builder-improvements-1.yml b/.changelogs/course-builder-improvements-1.yml new file mode 100644 index 0000000000..d5d40f0afe --- /dev/null +++ b/.changelogs/course-builder-improvements-1.yml @@ -0,0 +1,4 @@ +significance: minor +type: added +entry: Lesson content can be edited within the Course Builder for new lessons, + or existing lessons with no existing content. diff --git a/.changelogs/course-builder-improvements-2.yml b/.changelogs/course-builder-improvements-2.yml new file mode 100644 index 0000000000..37da4c8a94 --- /dev/null +++ b/.changelogs/course-builder-improvements-2.yml @@ -0,0 +1,3 @@ +significance: patch +type: fixed +entry: Close lesson settings panel when lesson has been trashed. diff --git a/.changelogs/course-builder-improvements.yml b/.changelogs/course-builder-improvements.yml new file mode 100644 index 0000000000..47f488c947 --- /dev/null +++ b/.changelogs/course-builder-improvements.yml @@ -0,0 +1,9 @@ +significance: minor +type: changed +links: + - "#3033" + - "#3056" + - "#3097" + - "#2938" + - "#3030" +entry: Various course builder fixes, with quizzes set to published by default. diff --git a/assets/js/builder/Controllers/Sync.js b/assets/js/builder/Controllers/Sync.js index 45b9a9accd..fc08219bf7 100644 --- a/assets/js/builder/Controllers/Sync.js +++ b/assets/js/builder/Controllers/Sync.js @@ -430,6 +430,18 @@ define( [], function() { model.set( 'id', info.id ); delete model._unsavedChanges.id; } + + if ( info.permalink ) { + model.set( 'permalink', info.permalink ); + } + if ( info.name ) { + model.set( 'name', info.name ); + } + + if ( info.content_added_in_builder ) { + model.set( 'content_added_in_builder', info.content_added_in_builder ); + } + maybe_restart_tracking( model, info ); // check children @@ -460,6 +472,20 @@ define( [], function() { model.set( 'id', info.id ); delete model._unsavedChanges.id; } + + // Update permalink and name if provided by the server. + if ( info.permalink ) { + model.set( 'permalink', info.permalink ); + } + if ( info.name ) { + model.set( 'name', info.name ); + } + + if ( info.content_added_in_builder ) { + model.set( 'content_added_in_builder', info.content_added_in_builder ); + } + + maybe_restart_tracking( model, info ); // check children diff --git a/assets/js/builder/Models/Lesson.js b/assets/js/builder/Models/Lesson.js index d3d8b22b12..722fba9099 100644 --- a/assets/js/builder/Models/Lesson.js +++ b/assets/js/builder/Models/Lesson.js @@ -76,6 +76,8 @@ define( [ 'Models/Quiz', 'Models/_Relationships', 'Models/_Utilities', 'Schemas/ quiz: {}, // Quiz model/data. quiz_enabled: 'no', + content_added_in_builder: '', + _forceSync: false, }; diff --git a/assets/js/builder/Models/Quiz.js b/assets/js/builder/Models/Quiz.js index 3ef1c9c25c..a2eb3cdd4e 100644 --- a/assets/js/builder/Models/Quiz.js +++ b/assets/js/builder/Models/Quiz.js @@ -64,7 +64,7 @@ define( [ type: 'llms_quiz', lesson_id: '', - status: 'draft', + status: 'publish', // editable fields. content: '', diff --git a/assets/js/builder/Schemas/Lesson.js b/assets/js/builder/Schemas/Lesson.js index f106d4db7f..ea5431bc1f 100644 --- a/assets/js/builder/Schemas/Lesson.js +++ b/assets/js/builder/Schemas/Lesson.js @@ -12,15 +12,34 @@ define( [], function() { title: LLMS.l10n.translate( 'General Settings' ), toggleable: true, fields: [ - [ - { - attribute: 'permalink', - id: 'permalink', - type: 'permalink', - }, - ], [ - { - attribute: 'video_embed', + [ + { + attribute: 'permalink', + id: 'permalink', + type: 'permalink', + }, + ], [ + { + attribute: 'content', + id: 'content', + label: LLMS.l10n.translate( 'Content' ), + type: 'editor', + condition: function() { + return '' === this.get( 'content' ) || 'yes' === this.get( 'content_added_in_builder' ); + }, + }, + ], [ + { + id: 'content-page-builder-notice', + label: LLMS.l10n.translate( 'Content' ), + type: 'page_builder_notice', + condition: function() { + return '' !== this.get( 'content' ) && 'yes' !== this.get( 'content_added_in_builder' ); + }, + }, + ], [ + { + attribute: 'video_embed', id: 'video-embed', label: LLMS.l10n.translate( 'Video Embed URL' ), type: 'video_embed', diff --git a/assets/js/builder/Views/Assignment.js b/assets/js/builder/Views/Assignment.js index 14c7c4b727..56b6777c63 100644 --- a/assets/js/builder/Views/Assignment.js +++ b/assets/js/builder/Views/Assignment.js @@ -114,6 +114,8 @@ define( [ */ this.model.set_parent( this.lesson ); + this.listenTo( this.model, 'change:permalink', this.render_settings ); + } this.on( 'model-trashed', this.on_trashed ); @@ -154,6 +156,23 @@ define( [ }, + /** + * Re-render the settings subview when permalink updates after saving. + * + * @since [version] + * + * @return {Void} + */ + render_settings: function() { + + var view = this.get_subview( 'settings' ); + if ( view && view.instance ) { + view.instance.render(); + this.init_selects(); + } + + }, + /** * Adds a new assignment to a lesson which currently has no assignment associated with it. * @@ -176,6 +195,7 @@ define( [ this.lesson.set( 'assignment_enabled', 'yes' ); this.lesson.set( 'assignment', this.model ); + this.listenTo( this.model, 'change:permalink', this.render_settings ); this.render(); } else { @@ -221,6 +241,7 @@ define( [ this.lesson.set( 'assignment', assignment ); this.model = assignment; + this.listenTo( this.model, 'change:permalink', this.render_settings ); this.render(); }, diff --git a/assets/js/builder/Views/Course.js b/assets/js/builder/Views/Course.js index 4b0995a4b4..a28cfb5415 100644 --- a/assets/js/builder/Views/Course.js +++ b/assets/js/builder/Views/Course.js @@ -89,6 +89,12 @@ define( [ Backbone.pubSub.on( 'lesson-selected', this.active_lesson_change, this ); + // Select the first section by default on load. + var firstSection = this.model.get( 'sections' ).first(); + if ( firstSection ) { + this.sectionListView.setSelectedModel( firstSection ); + } + }, /** @@ -167,8 +173,7 @@ define( [ */ on_section_toggle: function( model ) { - var selected = model.get( '_expanded' ) ? [ model ] : []; - this.sectionListView.setSelectedModels( selected ); + this.sectionListView.setSelectedModel( model ); }, diff --git a/assets/js/builder/Views/Elements.js b/assets/js/builder/Views/Elements.js index 1d4deb2c9d..71d5bae058 100644 --- a/assets/js/builder/Views/Elements.js +++ b/assets/js/builder/Views/Elements.js @@ -130,7 +130,9 @@ define( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'V event.preventDefault(); - var pop = new Popover( { + var pop, onLessonSelect; + + pop = new Popover( { el: '#llms-existing-lesson', args: { backdrop: true, @@ -144,13 +146,22 @@ define( [ 'Models/Section', 'Views/Section', 'Models/Lesson', 'Views/Lesson', 'V post_type: 'lesson', searching_message: LLMS.l10n.translate( 'Search for existing lessons...' ), } ).render().$el, + onHide: function() { + Backbone.pubSub.off( 'lesson-search-select', onLessonSelect ); + }, } } ); + onLessonSelect = function() { + pop.hide(); + + // Ref #3097 — pop.hide() doesn't always remove the DOM elements. + $( '.webui-popover' ).remove(); + $( '.webui-popover-backdrop' ).remove(); + }; + pop.show(); - Backbone.pubSub.on( 'lesson-search-select', function() { - pop.hide() - } ); + Backbone.pubSub.once( 'lesson-search-select', onLessonSelect ); }, diff --git a/assets/js/builder/Views/LessonEditor.js b/assets/js/builder/Views/LessonEditor.js index 869b17a34d..a08db42970 100644 --- a/assets/js/builder/Views/LessonEditor.js +++ b/assets/js/builder/Views/LessonEditor.js @@ -76,6 +76,9 @@ define( [ var change_events = window.llms.hooks.applyFilters( 'llms_lesson_rerender_change_events', [ 'change:date_available', 'change:drip_method', + 'change:permalink', + 'change:content_added_in_builder', + 'change:name', 'change:time_available', ] ); _.each( change_events, function( event ) { diff --git a/assets/js/builder/Views/Quiz.js b/assets/js/builder/Views/Quiz.js index 0d3385000b..65255d77dd 100644 --- a/assets/js/builder/Views/Quiz.js +++ b/assets/js/builder/Views/Quiz.js @@ -125,6 +125,8 @@ define( [ this.model.set_parent( this.lesson ); this.listenTo( this.model, 'change:_points', this.render_points ); + this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.listenTo( this.model, 'change:name', this.render_settings ); } @@ -226,6 +228,27 @@ define( [ }, + /** + * Re-render the settings subview. + * + * Used when the permalink is updated after saving so the settings + * panel reflects the new permalink without a full re-render. + * + * @since [version] + * + * @return {Void} + */ + render_settings: function() { + + var view = this.get_subview( 'settings' ); + if ( view && view.instance ) { + view.instance.render(); + this.init_datepickers(); + this.init_selects(); + } + + }, + /** * Bulk expand / collapse question buttons. * @@ -261,6 +284,9 @@ define( [ } this.model = quiz; + this.listenTo( this.model, 'change:_points', this.render_points ); + this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.listenTo( this.model, 'change:name', this.render_settings ); this.render(); }, @@ -298,6 +324,9 @@ define( [ this.lesson.add_quiz( quiz ); this.model = this.lesson.get( 'quiz' ); + this.listenTo( this.model, 'change:_points', this.render_points ); + this.listenTo( this.model, 'change:permalink', this.render_settings ); + this.listenTo( this.model, 'change:name', this.render_settings ); this.render(); }, diff --git a/assets/js/builder/Views/Section.js b/assets/js/builder/Views/Section.js index 3691f7d47c..a13ea8cf26 100644 --- a/assets/js/builder/Views/Section.js +++ b/assets/js/builder/Views/Section.js @@ -43,7 +43,6 @@ define( [ */ events: _.defaults( { - 'click': 'select', 'click .expand': 'expand', 'click .collapse': 'collapse', 'click .shift-up--section': 'shift_up', diff --git a/assets/js/builder/Views/SectionList.js b/assets/js/builder/Views/SectionList.js index 58b0485e74..6af1f18017 100644 --- a/assets/js/builder/Views/SectionList.js +++ b/assets/js/builder/Views/SectionList.js @@ -16,7 +16,7 @@ define( [ 'Views/Section', 'Views/_Receivable' ], function( SectionView, Receiva el: '#llms-sections', events : { - 'mousedown > li.llms-section > .llms-builder-header .llms-headline' : '_listItem_onMousedown', + 'mousedown > li.llms-section' : '_listItem_onMousedown', // 'dblclick > li, tbody > tr > td' : '_listItem_onDoubleClick', 'click' : '_listBackground_onClick', 'click ul.collection-view' : '_listBackground_onClick', diff --git a/assets/js/builder/Views/_Editable.js b/assets/js/builder/Views/_Editable.js index e94226c45b..aa16f45040 100644 --- a/assets/js/builder/Views/_Editable.js +++ b/assets/js/builder/Views/_Editable.js @@ -315,6 +315,8 @@ define( [], function() { */ on_select: function( event ) { + event.stopPropagation(); + var $el = $( event.target ), multi = ( $el.attr( 'multiple' ) ), attr = $el.attr( 'name' ), diff --git a/assets/js/builder/Views/_Trashable.js b/assets/js/builder/Views/_Trashable.js index 948a9176ac..6f94c72f66 100644 --- a/assets/js/builder/Views/_Trashable.js +++ b/assets/js/builder/Views/_Trashable.js @@ -49,6 +49,11 @@ define( [], function() { // publish event Backbone.pubSub.trigger( 'model-trashed', this.model ); + // close the editor sidebar if the trashed model is the one currently being edited + if ( this.model.get( '_selected' ) ) { + Backbone.pubSub.trigger( 'sidebar-editor-close' ); + } + // trigger local event so extending views can run other actions where necessary this.trigger( 'model-trashed', this.model ); diff --git a/assets/scss/admin/_course-builder.scss b/assets/scss/admin/_course-builder.scss index 29923cd2a8..f93b4c48e9 100644 --- a/assets/scss/admin/_course-builder.scss +++ b/assets/scss/admin/_course-builder.scss @@ -186,6 +186,9 @@ body.admin_page_llms-course-builder { .llms-lessons { overflow: visible; } } &.selected { + border-left: 3px solid $color-brand-blue; + box-shadow: 2px 2px 8px rgba( 0, 0, 0, 0.12 ); + margin-left: -2px; .llms-drag-utility.drag-section { border-color: $color-brand-blue; } diff --git a/includes/admin/class.llms.admin.builder.php b/includes/admin/class.llms.admin.builder.php index 58ff37a458..b123e30892 100644 --- a/includes/admin/class.llms.admin.builder.php +++ b/includes/admin/class.llms.admin.builder.php @@ -1073,6 +1073,17 @@ private static function update_lessons( $lessons, $section ) { $skip_props = apply_filters( 'llms_builder_update_lesson_skip_props', array( 'quiz' ) ); + // Don't overwrite content if the content editor doesn't display. + if ( ! $created && '' !== $lesson->get( 'content' ) && ! llms_parse_bool( $lesson->get( 'content_added_in_builder' ) ) ) { + $skip_props[] = 'content'; + } + + if ( '' === $lesson->get( 'content' ) && isset( $lesson_data['content'] ) && '' !== $lesson_data['content'] + && ! isset( $lesson_data['content_added_in_builder'] ) ) { + // We're adding content via the builder for the first time; add a flag saying so. + $lesson_data['content_added_in_builder'] = 'yes'; + } + // Update all updatable properties. foreach ( $properties as $prop ) { if ( isset( $lesson_data[ $prop ] ) && ! in_array( $prop, $skip_props, true ) ) { @@ -1097,6 +1108,11 @@ private static function update_lessons( $lessons, $section ) { $lesson->set( 'name', sanitize_title( $lesson_data['title'] ) ); } + // Include permalink, slug, and editor type in the response so the builder can update the model. + $res['permalink'] = get_permalink( $lesson->get( 'id' ) ); + $res['name'] = $lesson->get( 'name' ); + $res['content_added_in_builder'] = $lesson->get( 'content_added_in_builder' ); + // Remove revision prevention. remove_filter( 'wp_revisions_to_keep', '__return_zero', 999 ); @@ -1253,7 +1269,12 @@ private static function update_quiz( $quiz_data, $lesson ) { // Create a quiz. if ( self::is_temp_id( $quiz_data['id'] ) ) { - $quiz = new LLMS_Quiz( 'new' ); + $quiz = new LLMS_Quiz( + 'new', + array( + 'post_title' => isset( $quiz_data['title'] ) ? $quiz_data['title'] : __( 'New Quiz', 'lifterlms' ), + ) + ); // Update existing quiz. } else { @@ -1301,6 +1322,10 @@ private static function update_quiz( $quiz_data, $lesson ) { } } + // Include permalink and slug in the response so the builder can update the model. + $res['permalink'] = get_permalink( $quiz->get( 'id' ) ); + $res['name'] = $quiz->get( 'name' ); + if ( isset( $quiz_data['questions'] ) && is_array( $quiz_data['questions'] ) ) { $res['questions'] = self::update_questions( $quiz_data['questions'], $quiz ); } diff --git a/includes/admin/views/builder/settings-fields.php b/includes/admin/views/builder/settings-fields.php index 11f36382ce..f00a2ea88d 100644 --- a/includes/admin/views/builder/settings-fields.php +++ b/includes/admin/views/builder/settings-fields.php @@ -67,6 +67,15 @@ + <# } else if ( 'page_builder_notice' === field.type ) { #> + +
+ <# } else if ( 'upsell' === field.type ) { #> diff --git a/includes/models/model.llms.lesson.php b/includes/models/model.llms.lesson.php index d4f55b5715..99f3cbeaf0 100644 --- a/includes/models/model.llms.lesson.php +++ b/includes/models/model.llms.lesson.php @@ -42,6 +42,7 @@ * @property string $require_assignment_passing_grade Whether of not students have to pass the assignment to advance to the next lesson [yes|no]. * @property string $time_available Optional time to make lesson available on $date_available when $drip_method is "date". * @property string $video_embed URL to an oEmbed enable video URL. + * @property string $content_added_in_builder Whether content was (at least initially) added within the page builder. */ class LLMS_Lesson extends LLMS_Post_Model { @@ -68,6 +69,8 @@ class LLMS_Lesson extends LLMS_Post_Model { 'require_assignment_passing_grade' => 'yesno', 'points' => 'absint', + 'content_added_in_builder' => 'yesno', + // Quizzes. 'quiz' => 'absint', 'quiz_enabled' => 'yesno', diff --git a/tests/phpunit/unit-tests/class-llms-test-generator-courses.php b/tests/phpunit/unit-tests/class-llms-test-generator-courses.php index 7bcb35b782..38f4c55dab 100644 --- a/tests/phpunit/unit-tests/class-llms-test-generator-courses.php +++ b/tests/phpunit/unit-tests/class-llms-test-generator-courses.php @@ -395,7 +395,7 @@ public function test_create_lesson() { // Test meta props are set. foreach ( array_keys( LLMS_Unit_Test_Util::get_private_property_value( $lesson, 'properties' ) ) as $prop ) { // This data is not based off raw. - if ( in_array( $prop, array( 'order', 'parent_course', 'parent_section', 'quiz' ), true ) ) { + if ( in_array( $prop, array( 'order', 'parent_course', 'parent_section', 'quiz', 'content_added_in_builder' ), true ) ) { continue; } $this->assertEquals( $raw[ $prop ], $lesson->get( $prop ), $prop );