feat: provisionally support V2 libraries in LibraryContentBlock (randomized only) (#33263)
Refactors and reworks the LibraryContentBlock so that its sync-from-library operations are asynchronous and work with V2 content libraries. This also required us to make library_content block duplication asynchronous, as that involves syncing from the source library. For the sake of clarity, this PR includes two major method renames: * update_children(...) -> sync_from_library(...) * refresh_library(...) -> sync_from_library(upgrade_to_latest=True, ...) an an XBlock HTTP handler rename: /refresh_children -> /upgrade_and_sync There are still a couple issues with import or duplication of library_content blocks referencing V2 libraries other than latest. These will be resolved in an upcoming PR. Part of: https://openedx.atlassian.net/wiki/spaces/COMM/pages/3820617729/Spec+Memo+Content+Library+Authoring+Experience+V2 Follow-up work: https://github.com/openedx/edx-platform/issues/33640 Co-authored-by: Connor Haugh <chaugh@2u.com> Co-authored-by: Eugene Dyudyunov <evgen.dyudyunov@raccoongang.com>
This commit is contained in:
@@ -7,11 +7,14 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
|
||||
'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal',
|
||||
'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',
|
||||
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt', 'js/utils/module',
|
||||
],
|
||||
function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
|
||||
EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
|
||||
ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView) {
|
||||
function($, _, Backbone, gettext, BasePage,
|
||||
ViewUtils, ContainerView, XBlockView,
|
||||
AddXBlockComponent, EditXBlockModal, MoveXBlockModal,
|
||||
XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
|
||||
ContainerSubviews, UnitOutlineView, XBlockUtils,
|
||||
NotificationView, PromptView, ModuleUtils) {
|
||||
'use strict';
|
||||
|
||||
var XBlockContainerPage = BasePage.extend({
|
||||
@@ -26,7 +29,10 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
'click .delete-button': 'deleteXBlock',
|
||||
'click .show-actions-menu-button': 'showXBlockActionsMenu',
|
||||
'click .new-component-button': 'scrollToNewComponentButtons',
|
||||
'click .save-button': 'saveSelectedLibraryComponents',
|
||||
'click .paste-component-button': 'pasteComponent',
|
||||
'change .header-library-checkbox': 'toggleLibraryComponent',
|
||||
'click .collapse-button': 'collapseXBlock',
|
||||
},
|
||||
|
||||
options: {
|
||||
@@ -102,6 +108,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
model: this.model
|
||||
});
|
||||
this.unitOutlineView.render();
|
||||
|
||||
}
|
||||
|
||||
this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved);
|
||||
@@ -521,6 +528,78 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
});
|
||||
},
|
||||
|
||||
duplicateXBlock: function(event) {
|
||||
event.preventDefault();
|
||||
this.duplicateComponent(this.findXBlockElement(event.target));
|
||||
},
|
||||
|
||||
showMoveXBlockModal: function(event) {
|
||||
var xblockElement = this.findXBlockElement(event.target),
|
||||
parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'),
|
||||
modal = new MoveXBlockModal({
|
||||
sourceXBlockInfo: XBlockUtils.findXBlockInfo(xblockElement, this.model),
|
||||
sourceParentXBlockInfo: XBlockUtils.findXBlockInfo(parentXBlockElement, this.model),
|
||||
XBlockURLRoot: this.getURLRoot(),
|
||||
outlineURL: this.options.outlineURL
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
modal.show();
|
||||
},
|
||||
|
||||
deleteXBlock: function(event) {
|
||||
event.preventDefault();
|
||||
this.deleteComponent(this.findXBlockElement(event.target));
|
||||
},
|
||||
|
||||
createPlaceholderElement: function() {
|
||||
return $('<div/>', {class: 'studio-xblock-wrapper'});
|
||||
},
|
||||
|
||||
createComponent: function(template, target) {
|
||||
// A placeholder element is created in the correct location for the new xblock
|
||||
// and then onNewXBlock will replace it with a rendering of the xblock. Note that
|
||||
// for xblocks that can't be replaced inline, the entire parent will be refreshed.
|
||||
var parentElement = this.findXBlockElement(target),
|
||||
parentLocator = parentElement.data('locator'),
|
||||
buttonPanel = target.closest('.add-xblock-component'),
|
||||
listPanel = buttonPanel.prev(),
|
||||
scrollOffset = ViewUtils.getScrollOffset(buttonPanel),
|
||||
$placeholderEl = $(this.createPlaceholderElement()),
|
||||
requestData = _.extend(template, {
|
||||
parent_locator: parentLocator
|
||||
}),
|
||||
placeholderElement;
|
||||
placeholderElement = $placeholderEl.appendTo(listPanel);
|
||||
return $.postJSON(this.getURLRoot() + '/', requestData,
|
||||
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
|
||||
.fail(function() {
|
||||
// Remove the placeholder if the update failed
|
||||
placeholderElement.remove();
|
||||
});
|
||||
},
|
||||
|
||||
duplicateComponent: function(xblockElement) {
|
||||
// A placeholder element is created in the correct location for the duplicate xblock
|
||||
// and then onNewXBlock will replace it with a rendering of the xblock. Note that
|
||||
// for xblocks that can't be replaced inline, the entire parent will be refreshed.
|
||||
var self = this,
|
||||
parentElement = self.findXBlockElement(xblockElement.parent()),
|
||||
scrollOffset = ViewUtils.getScrollOffset(xblockElement),
|
||||
$placeholderEl = $(self.createPlaceholderElement()),
|
||||
placeholderElement;
|
||||
|
||||
placeholderElement = $placeholderEl.insertAfter(xblockElement);
|
||||
XBlockUtils.duplicateXBlock(xblockElement, parentElement)
|
||||
.done(function(data) {
|
||||
self.onNewXBlock(placeholderElement, scrollOffset, true, data);
|
||||
})
|
||||
.fail(function() {
|
||||
// Remove the placeholder if the update failed
|
||||
placeholderElement.remove();
|
||||
});
|
||||
},
|
||||
|
||||
deleteComponent: function(xblockElement) {
|
||||
var self = this,
|
||||
xblockInfo = new XBlockInfo({
|
||||
@@ -531,6 +610,61 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
});
|
||||
},
|
||||
|
||||
getSelectedLibraryComponents: function() {
|
||||
var self = this;
|
||||
var locator = this.$el.find('.studio-xblock-wrapper').data('locator');
|
||||
console.log(ModuleUtils);
|
||||
$.getJSON(
|
||||
ModuleUtils.getUpdateUrl(locator) + '/handler/get_block_ids',
|
||||
function(data) {
|
||||
self.selectedLibraryComponents = Array.from(data.source_block_ids);
|
||||
self.storedSelectedLibraryComponents = Array.from(data.source_block_ids);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
saveSelectedLibraryComponents: function(e) {
|
||||
var self = this;
|
||||
var locator = this.$el.find('.studio-xblock-wrapper').data('locator');
|
||||
e.preventDefault();
|
||||
$.postJSON(
|
||||
ModuleUtils.getUpdateUrl(locator) + '/handler/submit_studio_edits',
|
||||
{values: {source_block_ids: self.storedSelectedLibraryComponents}},
|
||||
function() {
|
||||
self.selectedLibraryComponents = Array.from(self.storedSelectedLibraryComponents);
|
||||
self.toggleSaveButton();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
toggleLibraryComponent: function(event) {
|
||||
var componentId = $(event.target).closest('.studio-xblock-wrapper').data('locator');
|
||||
var storeIndex = this.storedSelectedLibraryComponents.indexOf(componentId);
|
||||
if (storeIndex > -1) {
|
||||
this.storedSelectedLibraryComponents.splice(storeIndex, 1);
|
||||
this.toggleSaveButton();
|
||||
} else {
|
||||
this.storedSelectedLibraryComponents.push(componentId);
|
||||
this.toggleSaveButton();
|
||||
}
|
||||
},
|
||||
|
||||
toggleSaveButton: function() {
|
||||
var $saveButton = $('.nav-actions .save-button');
|
||||
if (JSON.stringify(this.selectedLibraryComponents.sort()) === JSON.stringify(this.storedSelectedLibraryComponents.sort())) {
|
||||
$saveButton.addClass('is-hidden');
|
||||
window.removeEventListener('beforeunload', this.onBeforePageUnloadCallback);
|
||||
} else {
|
||||
$saveButton.removeClass('is-hidden');
|
||||
window.addEventListener('beforeunload', this.onBeforePageUnloadCallback);
|
||||
}
|
||||
},
|
||||
|
||||
onBeforePageUnloadCallback: function (event) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
},
|
||||
|
||||
onDelete: function(xblockElement) {
|
||||
// get the parent so we can remove this component from its parent.
|
||||
var xblockView = this.xblockView,
|
||||
|
||||
@@ -223,6 +223,10 @@
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
background-color: $white;
|
||||
|
||||
&-fullwidth {
|
||||
width: flex-grid(12, 12);
|
||||
}
|
||||
}
|
||||
|
||||
.content-supplementary {
|
||||
|
||||
@@ -43,6 +43,19 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.header-library-checkbox {
|
||||
margin-right: 10px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-library-checkbox-label {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-details {
|
||||
@extend %cont-truncated;
|
||||
|
||||
@@ -433,7 +446,17 @@
|
||||
border-color: $blue;
|
||||
}
|
||||
|
||||
.xblock-header {
|
||||
&.is-collapsed {
|
||||
.xblock-render {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse-button .fa {
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
.xblock-header:not(.is-hidden) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user