From 4158a44307ca20070589ada96a1c53eb71daaea7 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 23 Oct 2024 05:29:16 -0700 Subject: [PATCH] feat: multi-select when adding blocks to problem bank (#35705) This implements basic multi-select for adding components to a problem bank, for the Libraries Relaunch Beta [FC-0062]. Part of: https://github.com/openedx/frontend-app-authoring/issues/1385 --- .../views/components/add_library_content.js | 7 ++++ .../views/modals/select_v2_library_content.js | 36 ++++++++++++++---- cms/static/js/views/pages/container.js | 38 ++++++++++++------- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/cms/static/js/views/components/add_library_content.js b/cms/static/js/views/components/add_library_content.js index ee1894b8aa..278717ba92 100644 --- a/cms/static/js/views/components/add_library_content.js +++ b/cms/static/js/views/components/add_library_content.js @@ -1,6 +1,13 @@ /** * Provides utilities to open and close the library content picker. + * This is for adding a single, selected, non-randomized component (XBlock) + * from the library into the course. It achieves the same effect as copy-pasting + * the block from a library into the course. The block will remain synced with + * the "upstream" library version. * + * Compare cms/static/js/views/modals/select_v2_library_content.js which uses + * a multi-select modal to add component(s) to a Problem Bank (for + * randomization). */ define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal'], function($, _, gettext, BaseModal) { diff --git a/cms/static/js/views/modals/select_v2_library_content.js b/cms/static/js/views/modals/select_v2_library_content.js index 8752367967..76fb301540 100644 --- a/cms/static/js/views/modals/select_v2_library_content.js +++ b/cms/static/js/views/modals/select_v2_library_content.js @@ -1,6 +1,9 @@ /** * Provides utilities to open and close the library content picker. + * This is for adding multiple components to a Problem Bank (for randomization). * + * Compare cms/static/js/views/components/add_library_content.js which uses + * a single-select modal to add one component to a course (non-randomized). */ define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal'], function($, _, gettext, BaseModal) { @@ -8,26 +11,31 @@ function($, _, gettext, BaseModal) { var SelectV2LibraryContent = BaseModal.extend({ options: $.extend({}, BaseModal.prototype.options, { - modalName: 'add-component-from-library', + modalName: 'add-components-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, }), + events: { + 'click .action-add': 'addSelectedComponents', + 'click .action-cancel': 'cancel', + }, + initialize: function() { BaseModal.prototype.initialize.call(this); + this.selections = []; // 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, + if (event.data?.type === 'pickerSelectionChanged') { + this.selections = event.data.selections; + if (this.selections.length > 0) { + this.enableActionButton('add'); + } else { + this.disableActionButton('add'); } - this.callback(requestData); - this.hide(); } }; this.messageListener = window.addEventListener("message", handleMessage); @@ -43,7 +51,19 @@ function($, _, gettext, BaseModal) { * Adds the action buttons to the modal. */ addActionButtons: function() { + this.addActionButton('add', gettext('Add selected components'), true); this.addActionButton('cancel', gettext('Cancel')); + this.disableActionButton('add'); + }, + + /** Handler when the user clicks the "Add Selected Components" primary button */ + addSelectedComponents: function(event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); // Make sure parent modals don't see the click + } + this.hide(); + this.callback(this.selections); }, /** diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 6a849aebe6..69b28e920b 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -426,6 +426,7 @@ function($, _, Backbone, gettext, BasePage, }); }, + /** Show the modal for previewing changes before syncing a library-sourced XBlock. */ showXBlockLibraryChangesPreview: function(event, options) { event.preventDefault(); @@ -438,6 +439,7 @@ function($, _, Backbone, gettext, BasePage, }); }, + /** Show the multi-select library content picker, for adding to a Problem Bank (itembank) Component */ showSelectV2LibraryContent: function(event, options) { event.preventDefault(); @@ -445,32 +447,40 @@ function($, _, Backbone, gettext, BasePage, const modal = new SelectV2LibraryContent(options); const courseAuthoringMfeUrl = this.model.attributes.course_authoring_url; const itemBankBlockId = xblockElement.data("locator"); - const pickerUrl = courseAuthoringMfeUrl + '/component-picker?variant=published'; + const pickerUrl = courseAuthoringMfeUrl + '/component-picker/multiple?variant=published'; - 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); }; + modal.showComponentPicker(pickerUrl, (selectedBlocks) => { + // selectedBlocks has type: {usageKey: string, blockType: string}[] + let doneAddingAllBlocks = () => { this.refreshXBlock(xblockElement, false); }; + let doneAddingBlock = () => {}; 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); + const $placeholderEl = $(this.createPlaceholderElement()); + const placeholderElement = $placeholderEl.insertBefore($insertSpot); placeholderElement.data('locator', addResult.locator); return this.refreshXBlock(placeholderElement, true); }; + doneAddingAllBlocks = () => {}; + } + // Note: adding all the XBlocks in parallel will cause a race condition 😢 so we have to add + // them one at a time: + let lastAdded = $.when(); + for (const { usageKey, blockType } of selectedBlocks) { + const addData = { + library_content_key: usageKey, + category: blockType, + parent_locator: itemBankBlockId, + }; + lastAdded = lastAdded.then(() => ( + $.postJSON(this.getURLRoot() + '/', addData, doneAddingBlock) + )); } // Now we actually add the block: ViewUtils.runOperationShowingMessage(gettext('Adding'), () => { - return $.postJSON(this.getURLRoot() + '/', createData, doneAddingBlock); + return lastAdded.done(() => { doneAddingAllBlocks() }); }); }); },