Skip to content

Commit

Permalink
feat: multi-select when adding blocks to problem bank (#35705)
Browse files Browse the repository at this point in the history
This implements basic multi-select for adding components to a problem bank,
for the Libraries Relaunch Beta [FC-0062].

Part of: openedx/frontend-app-authoring#1385
  • Loading branch information
bradenmacdonald authored Oct 23, 2024
1 parent e0c56aa commit 4158a44
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 23 deletions.
7 changes: 7 additions & 0 deletions cms/static/js/views/components/add_library_content.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
36 changes: 28 additions & 8 deletions cms/static/js/views/modals/select_v2_library_content.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
/**
* 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) {
'use strict';

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);
Expand All @@ -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);
},

/**
Expand Down
40 changes: 25 additions & 15 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -438,39 +439,48 @@ 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();

const xblockElement = this.findXBlockElement(event.target);
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';

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); };
const pickerUrl = courseAuthoringMfeUrl + '/component-picker/multiple?variant=published';

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() });
});
});
},
Expand Down

0 comments on commit 4158a44

Please sign in to comment.