diff --git a/cms/static/js/views/modals/select_v2_library_content.js b/cms/static/js/views/modals/select_v2_library_content.js new file mode 100644 index 000000000000..87523679678c --- /dev/null +++ b/cms/static/js/views/modals/select_v2_library_content.js @@ -0,0 +1,68 @@ +/** + * Provides utilities to open and close the library content picker. + * + */ +define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal'], +function($, _, gettext, BaseModal) { + 'use strict'; + + var SelectV2LibraryContent = BaseModal.extend({ + options: $.extend({}, BaseModal.prototype.options, { + modalName: 'add-component-from-library', + modalSize: 'lg', + view: 'studio_view', + viewSpecificClasses: 'modal-add-component-picker confirm', + // Translators: "title" is the name of the current component being edited. + titleFormat: gettext('Add library content'), + addPrimaryActionButton: false, + }), + + initialize: function() { + BaseModal.prototype.initialize.call(this); + // Add event listen to close picker when the iframe tells us to + const handleMessage = (event) => { + if (event.data?.type === 'pickerComponentSelected') { + var requestData = { + library_content_key: event.data.usageKey, + category: event.data.category, + } + this.callback(requestData); + this.hide(); + } + }; + this.messageListener = window.addEventListener("message", handleMessage); + this.cleanupListener = () => { window.removeEventListener("message", handleMessage) }; + }, + + hide: function() { + BaseModal.prototype.hide.call(this); + this.cleanupListener(); + }, + + /** + * Adds the action buttons to the modal. + */ + addActionButtons: function() { + this.addActionButton('cancel', gettext('Cancel')); + }, + + /** + * Show a component picker modal from library. + * @param contentPickerUrl Url for component picker + * @param callback A function to call with the selected block(s) + */ + showComponentPicker: function(contentPickerUrl, callback) { + this.contentPickerUrl = contentPickerUrl; + this.callback = callback; + + this.render(); + this.show(); + }, + + getContentHtml: function() { + return ``; + }, + }); + + return SelectV2LibraryContent; +}); diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index c49c2439473e..52418a503674 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -8,7 +8,8 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page 'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor', 'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils', 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt', - 'js/views/utils/tagging_drawer_utils', 'js/utils/module', 'js/views/modals/preview_v2_library_changes' + 'js/views/utils/tagging_drawer_utils', 'js/utils/module', 'js/views/modals/preview_v2_library_changes', + 'js/views/modals/select_v2_library_content' ], function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, @@ -16,7 +17,7 @@ function($, _, Backbone, gettext, BasePage, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor, ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView, TaggingDrawerUtils, ModuleUtils, - PreviewLibraryChangesModal) { + PreviewLibraryChangesModal, SelectV2LibraryContent) { 'use strict'; var XBlockContainerPage = BasePage.extend({ @@ -30,6 +31,7 @@ function($, _, Backbone, gettext, BasePage, 'click .move-button': 'showMoveXBlockModal', 'click .delete-button': 'deleteXBlock', 'click .library-sync-button': 'showXBlockLibraryChangesPreview', + 'click .problem-bank-v2-add-button': 'showSelectV2LibraryContent', 'click .show-actions-menu-button': 'showXBlockActionsMenu', 'click .new-component-button': 'scrollToNewComponentButtons', 'click .save-button': 'saveSelectedLibraryComponents', @@ -255,6 +257,7 @@ function($, _, Backbone, gettext, BasePage, } else { // The thing in the clipboard can be pasted into this unit: const detailsPopupEl = this.$(".clipboard-details-popup")[0]; + if (!detailsPopupEl) return; // This happens on the Problem Bank container page - no paste button is there anyways detailsPopupEl.querySelector(".detail-block-name").innerText = data.content.display_name; detailsPopupEl.querySelector(".detail-block-type").innerText = data.content.block_type_display; detailsPopupEl.querySelector(".detail-course-name").innerText = data.source_context_title; @@ -435,6 +438,44 @@ function($, _, Backbone, gettext, BasePage, }); }, + showSelectV2LibraryContent: function(event, options) { + event.preventDefault(); + + const xblockElement = this.findXBlockElement(event.target); + const modal = new SelectV2LibraryContent(options); + const courseAuthoringMfeUrl = this.model.attributes.course_authoring_url; + const itemBankBlockId = xblockElement.data("locator"); + // TODO:: the ?parentLocator param shouldn't be necessary but is currently required by the component picker + const pickerUrl = courseAuthoringMfeUrl + '/component-picker?parentLocator=' + encodeURIComponent(itemBankBlockId); + + modal.showComponentPicker(pickerUrl, (selectedBlockData) => { + const createData = { + parent_locator: itemBankBlockId, + // The user wants to add this block from the library to the Problem Bank: + library_content_key: selectedBlockData.library_content_key, + category: selectedBlockData.category, + }; + let doneAddingBlock = () => { this.refreshXBlock(xblockElement, false); }; + if (this.model.id === itemBankBlockId) { + // We're on the detailed view, showing all the components inside the problem bank. + // Create a placeholder that will become the new block(s) + const $placeholderEl = $(this.createPlaceholderElement()); + const $insertSpot = xblockElement.find('.insert-new-lib-blocks-here'); + const placeholderElement = $placeholderEl.insertBefore($insertSpot); + const scrollOffset = ViewUtils.getScrollOffset($placeholderEl); + doneAddingBlock = (addResult) => { + ViewUtils.setScrollOffset(placeholderElement, scrollOffset); + placeholderElement.data('locator', addResult.locator); + return this.refreshXBlock(placeholderElement, true); + }; + } + // Now we actually add the block: + ViewUtils.runOperationShowingMessage(gettext('Adding'), () => { + return $.postJSON(this.getURLRoot() + '/', createData, doneAddingBlock); + }); + }); + }, + /** * If the new "Actions" menu is enabled, most XBlock actions like * Duplicate, Move, Delete, Manage Access, etc. are moved into this diff --git a/xmodule/item_bank_block.py b/xmodule/item_bank_block.py index e7b1e3bdb0bb..5c81cbf89399 100644 --- a/xmodule/item_bank_block.py +++ b/xmodule/item_bank_block.py @@ -17,7 +17,9 @@ from xblock.completable import XBlockCompletionMode from xblock.core import XBlock from xblock.fields import Boolean, Integer, List, Scope, String +from xblock.utils.resources import ResourceLoader +from xmodule.block_metadata_utils import display_name_with_default from xmodule.mako_block import MakoTemplateBlockBase from xmodule.studio_editable import StudioEditableBlock from xmodule.util.builtin_assets import add_webpack_js_to_fragment @@ -34,6 +36,7 @@ _ = lambda text: text logger = logging.getLogger(__name__) +loader = ResourceLoader(__name__) @XBlock.needs('mako') @@ -441,29 +444,19 @@ def validate(self): validation = super().validate() if not isinstance(validation, StudioValidation): validation = StudioValidation.copy(validation) - if not validation.empty: - pass # If there's already a validation error, leave it there. - elif not self.children: - validation.set_summary( - StudioValidationMessage( - StudioValidationMessage.WARNING, - (_('No problems have been selected.')), - action_class='edit-button', - action_label=_("Select problems to randomize.") + if not validation.empty: # If there's already a validation error, leave it there. + if len(self.children) > 0 and len(self.children) < self.max_count: + validation.set_summary( + StudioValidationMessage( + StudioValidationMessage.WARNING, + _( + "The problem bank has been configured to show {count} problems, " + "but only {actual} have been selected." + ).format(count=self.max_count, actual=len(self.children)), + action_class='edit-button', + action_label=_("Edit the problem bank configuration.") + ) ) - ) - elif len(self.children) < self.max_count: - validation.set_summary( - StudioValidationMessage( - StudioValidationMessage.WARNING, - _( - "The problem bank has been configured to show {count} problems, " - "but only {actual} have been selected." - ).format(count=self.max_count, actual=len(self.children)), - action_class='edit-button', - action_label=_("Edit the problem bank configuration."), - ) - ) return validation def author_view(self, context): @@ -475,20 +468,31 @@ def author_view(self, context): fragment = Fragment() root_xblock = context.get('root_xblock') is_root = root_xblock and root_xblock.location == self.location - # User has clicked the "View" link. Show a preview of all possible children: - if is_root and self.children: # pylint: disable=no-member - fragment.add_content(self.runtime.service(self, 'mako').render_cms_template( - "library-block-author-preview-header.html", { - 'max_count': self.max_count if self.max_count >= 0 else len(self.children), - 'display_name': self.display_name or self.url_name, - })) + if is_root and self.children: + # User has clicked the "View" link. Show a preview of all possible children: + max_count = self.max_count + if max_count < 0: + max_count = len(self.children) context['can_edit_visibility'] = False context['can_move'] = False context['can_collapse'] = True self.render_children(context, fragment, can_reorder=False, can_add=False) - context['is_loading'] = False - - fragment.initialize_js('LibraryContentAuthorView') + else: + # We're just on the regular unit page, or we're on the "view" page but no children exist yet. + # Show a summary message and instructions. + summary_html = loader.render_django_template('templates/item_bank/author_view.html', { + "item_bank_id": self.usage_key, + "blocks": [ + {"display_name": display_name_with_default(child)} + for child in self.get_children() + ], + "block_count": len(self.children), + "max_count": self.max_count, + }) + fragment.add_content(summary_html) + # Whether on the main author view or the detailed children view, show a button to add more from the library: + add_html = loader.render_django_template('templates/item_bank/author_view_add.html', {}) + fragment.add_content(add_html) return fragment def format_block_keys_for_analytics(self, block_keys: list[tuple[str, str]]) -> list[dict]: diff --git a/xmodule/templates/item_bank/author_view.html b/xmodule/templates/item_bank/author_view.html new file mode 100644 index 000000000000..40b27272ba4d --- /dev/null +++ b/xmodule/templates/item_bank/author_view.html @@ -0,0 +1,14 @@ +
Learners will see {{ max_count }} of the {{ block_count }} selected components:
+Press View to preview, sync/update, and/or remove the selected components.
+Press Edit to configure how many will be shown and other settings.
+ {% else %} +You have not selected any components yet.
+ {% endif %} +