Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cms/djangoapps/contentstore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ def xblock_type_display_name(xblock, default_display_name=None):
# description like "Multiple Choice Problem", but that won't work if our 'block' argument is just the block_type
# string ("problem").
return _('Problem')
elif category == 'library_v2':
return _('Library Content')
component_class = XBlock.load_class(category)
if hasattr(component_class, 'display_name') and component_class.display_name.default:
return _(component_class.display_name.default) # lint-amnesty, pylint: disable=translation-of-non-string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class ContainerHandlerSerializer(serializers.Serializer):
unit_block_id = serializers.CharField(source="unit.location.block_id")
subsection_location = serializers.CharField(source="subsection.location")
course_sequence_ids = serializers.ListField(child=serializers.CharField())
library_content_picker_url = serializers.CharField()

def get_assets_url(self, obj):
"""
Expand Down
8 changes: 5 additions & 3 deletions cms/djangoapps/contentstore/rest_api/v2/views/downstreams.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,13 @@ def put(self, request: _AuthenticatedRequest, usage_key_string: str) -> Response
# Note that, if this fails and we raise a 4XX, then we will not call modulstore().update_item,
# thus preserving the former value of `downstream.upstream`.
downstream.upstream = new_upstream_ref
sync_param = request.data.get("sync", "false").lower()
if sync_param not in ["true", "false"]:
sync_param = request.data.get("sync", "false")
if isinstance(sync_param, str):
sync_param = sync_param.lower()
if sync_param not in ["true", "false", True, False]:
raise ValidationError({"sync": "must be 'true' or 'false'"})
try:
if sync_param == "true":
if sync_param == "true" or sync_param is True:
sync_from_upstream(downstream=downstream, user=request.user)
else:
# Even if we're not syncing (i.e., updating the downstream's values with the upstream's), we still need
Expand Down
12 changes: 12 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,17 @@ def get_course_outline_url(course_locator) -> str:
return course_outline_url


def get_library_content_picker_url(block_locator) -> str:
"""
Gets course authoring microfrontend library content picker URL for the given parent block.
"""
course_locator = block_locator.course_key
mfe_base_url = get_course_authoring_url(course_locator)
content_picker_url = f'{mfe_base_url}/component-picker?parentLocator={block_locator}'
Comment thread
bradenmacdonald marked this conversation as resolved.
Outdated

return content_picker_url


def get_unit_url(course_locator, unit_locator) -> str:
"""
Gets course authoring microfrontend URL for unit page view.
Expand Down Expand Up @@ -2045,6 +2056,7 @@ def get_container_handler_context(request, usage_key, course, xblock): # pylint
'user_clipboard': user_clipboard,
'is_fullwidth_content': is_library_xblock,
'course_sequence_ids': course_sequence_ids,
'library_content_picker_url': get_library_content_picker_url(xblock.location),
}
return context

Expand Down
2 changes: 2 additions & 0 deletions cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ def xblock_handler(request, usage_key_string=None):
if duplicate_source_locator is not present
:staged_content: use "clipboard" to paste from the OLX user's clipboard. (Incompatible with all other
fields except parent_locator)
:library_content_key: the key of the library content to add. (Incompatible with
all other fields except parent_locator)
Comment on lines +138 to +139

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having this POST arg will be nice, so that we don't have to follow up the request with a PUT 👍🏻

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kdmccormick Yes, called upstream sync after creating the block in a single call, simplified the js part. bc27ba3

The locator (unicode representation of a UsageKey) for the created xblock (minus children) is returned.
"""
return handle_xblock(request, usage_key_string)
Expand Down
20 changes: 17 additions & 3 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,16 @@
log = logging.getLogger(__name__)

# NOTE: This list is disjoint from ADVANCED_COMPONENT_TYPES
COMPONENT_TYPES = ['discussion', 'library', 'html', 'openassessment', 'problem', 'video', 'drag-and-drop-v2']
COMPONENT_TYPES = [
'discussion',
'library',
'library_v2',
Comment thread
bradenmacdonald marked this conversation as resolved.
Outdated
'html',
'openassessment',
'problem',
'video',
'drag-and-drop-v2',
]

ADVANCED_COMPONENT_TYPES = sorted({name for name, class_ in XBlock.load_classes()} - set(COMPONENT_TYPES))

Expand Down Expand Up @@ -97,6 +106,10 @@ def _load_mixed_class(category):
"""
Load an XBlock by category name, and apply all defined mixins
"""
# Libraries v2 content doesn't have an XBlock.
if category == 'library_v2':
return None

component_class = XBlock.load_class(category)
mixologist = Mixologist(settings.XBLOCK_MIXINS)
return mixologist.mix(component_class)
Expand Down Expand Up @@ -247,7 +260,8 @@ def create_support_legend_dict():
'problem': _("Problem"),
'video': _("Video"),
'openassessment': _("Open Response"),
'library': _("Library Content"),
'library': _("Legacy Library"),
'library_v2': _("Library Content"),
'drag-and-drop-v2': _("Drag and Drop"),
}

Expand Down Expand Up @@ -277,7 +291,7 @@ def create_support_legend_dict():
templates_for_category = []
component_class = _load_mixed_class(category)

if support_level_without_template and category != 'library':
if support_level_without_template and category not in ['library']:
# add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from cms.djangoapps.contentstore.toggles import ENABLE_DEFAULT_ADVANCED_PROBLEM_EDITOR_FLAG
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.lib.ai_aside_summary_config import AiAsideSummaryConfig
from cms.lib.xblock.upstream_sync import BadUpstream, sync_from_upstream
from common.djangoapps.static_replace import replace_static_urls
from common.djangoapps.student.auth import (
has_studio_read_access,
Expand Down Expand Up @@ -586,6 +587,20 @@ def _create_block(request):
boilerplate=request.json.get("boilerplate"),
)

# If it contains library_content_key, the block is being imported from a v2 library
# so it needs to be synced with upstream block.
if upstream_ref := request.json.get("library_content_key"):
try:
# Set `created_block.upstream` so that we can try to sync and/or fetch.
# Note that, if this fails and we raise a 4XX, then we will not call modulstore().update_item,
# thus preserving the former value of `created_block.upstream`.
Comment thread
bradenmacdonald marked this conversation as resolved.
Outdated
created_block.upstream = upstream_ref
sync_from_upstream(downstream=created_block, user=request.user)
except BadUpstream:
_delete_item(created_block.location, request.user)
return JsonResponse({"error": _("Invalid library xblock reference.")}, status=400)
modulestore().update_item(created_block, request.user.id)

return JsonResponse(
{
"locator": str(created_block.location),
Expand Down
Binary file added cms/static/images/large-library_v2-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions cms/static/js/views/components/add_library_content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* 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 AddLibraryContent = 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
window.addEventListener("message", function (event) {
if (event.data?.type === 'pickerComponentSelected') {
var requestData = {
library_content_key: event.data.usageKey,
category: event.data.category,
}
this.refreshFunction(requestData);
this.hide();
}
}.bind(this), { once: true }, false);
Comment thread
bradenmacdonald marked this conversation as resolved.
Outdated
},

/**
* Adds the action buttons to the modal.
*/
addActionButtons: function() {
this.addActionButton('cancel', gettext('Cancel'));

@bradenmacdonald bradenmacdonald Oct 19, 2024 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be fixed in a fast-follow PR (because I'm eager to get this into testing ASAP), but it's not a great UX that the "Next" button is sometimes hidden until you scroll, and the Next + Cancel buttons are in totally different places. I think they need to either be both inside the modal or both in the legy UI actions bar.
sceenshot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, do we even need a Next button? Selecting a course should directly take to next page since we only have one item to select in the first slide.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed next button and updated it to go to component selection on library selection.

vokoscreenNG-2024-10-21_11-16-03.mp4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me!

},

/**
* Show a component picker modal from library.
* @param contentPickerUrl Url for component picker
* @param refreshFunction A function to refresh the block after it has been updated
*/
showComponentPicker: function(contentPickerUrl, refreshFunction) {
this.contentPickerUrl = contentPickerUrl;
this.refreshFunction = refreshFunction;

this.render();
this.show();
},

getContentHtml: function() {
return `<iframe src="${this.contentPickerUrl}" onload="this.contentWindow.focus()" frameborder="0" style="width: 100%; height: 100%;"/>`;
},
});

return AddLibraryContent;
});
37 changes: 28 additions & 9 deletions cms/static/js/views/components/add_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
*/
define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils',
'js/views/components/add_xblock_button', 'js/views/components/add_xblock_menu',
'js/views/components/add_library_content',
'edx-ui-toolkit/js/utils/html-utils'],
function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, HtmlUtils) {
function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, AddLibraryContent, HtmlUtils) {
'use strict';

var AddXBlockComponent = BaseView.extend({
Expand Down Expand Up @@ -67,14 +68,32 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Htm
oldOffset = ViewUtils.getScrollOffset(this.$el);
event.preventDefault();
this.closeNewComponent(event);
ViewUtils.runOperationShowingMessage(
gettext('Adding'),
_.bind(this.options.createComponent, this, saveData, $element)
).always(function() {
// Restore the scroll position of the buttons so that the new
// component appears above them.
ViewUtils.setScrollOffset(self.$el, oldOffset);
});

if (saveData.type === 'library_v2') {
var modal = new AddLibraryContent();
modal.showComponentPicker(
this.options.libraryContentPickerUrl,
function(data) {
ViewUtils.runOperationShowingMessage(
gettext('Adding'),
_.bind(this.options.createComponent, this, data, $element),
).always(function() {
// Restore the scroll position of the buttons so that the new
// component appears above them.
ViewUtils.setScrollOffset(self.$el, oldOffset);
});
}.bind(this)
);
} else {
ViewUtils.runOperationShowingMessage(
gettext('Adding'),
_.bind(this.options.createComponent, this, saveData, $element),
).always(function() {
// Restore the scroll position of the buttons so that the new
// component appears above them.
ViewUtils.setScrollOffset(self.$el, oldOffset);
});
}
}
});

Expand Down
7 changes: 4 additions & 3 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ function($, _, Backbone, gettext, BasePage,
var component = new AddXBlockComponent({
el: element,
createComponent: _.bind(self.createComponent, self),
collection: self.options.templates
collection: self.options.templates,
libraryContentPickerUrl: self.options.libraryContentPickerUrl,
});
component.render();
});
Expand All @@ -224,7 +225,7 @@ function($, _, Backbone, gettext, BasePage,
},

initializePasteButton() {
if (this.options.canEdit && !self.options.isIframeEmbed) {
if (this.options.canEdit && !this.options.isIframeEmbed) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes a typo/bug introduced in #35587

// We should have the user's clipboard status.
const data = this.options.clipboardData;
this.refreshPasteButton(data);
Expand All @@ -241,7 +242,7 @@ function($, _, Backbone, gettext, BasePage,
refreshPasteButton(data) {
// Do not perform any changes on paste button since they are not
// rendered on Library or LibraryContent pages
if (!this.isLibraryPage && !this.isLibraryContentPage && !self.options.isIframeEmbed) {
if (!this.isLibraryPage && !this.isLibraryContentPage && !this.options.isIframeEmbed) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes a typo/bug introduced in #35587

// 'data' is the same data returned by the "get clipboard status" API endpoint
// i.e. /api/content-staging/v1/clipboard/
if (this.options.canEdit && data.content) {
Expand Down
7 changes: 7 additions & 0 deletions cms/static/sass/assets/_graphics.scss
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,10 @@
height: ($baseline*3);
background: url('#{$static-path}/images/large-library-icon.png') center no-repeat;
}

.large-library_v2-icon {
display: inline-block;
width: ($baseline*3);
height: ($baseline*3);
background: url('#{$static-path}/images/large-library_v2-icon.png') center no-repeat;
}
22 changes: 19 additions & 3 deletions cms/static/sass/elements/_drawer.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// studio - elements - side drawers
// ====================

.drawer-cover {
.drawer-cover,
.picker-cover {
@extend %ui-depth3;

display: none;
Expand All @@ -13,7 +14,8 @@
background: rgba(0, 0, 0, 0.8);
}

.drawer-cover.gray-cover {
.drawer-cover.gray-cover,
.picker-cover.gray-cover {
background: rgba(112, 112, 112, 0.8);
}

Expand All @@ -29,6 +31,20 @@
background-color: $gray-l4;
}

body.drawer-open {
body.drawer-open,
body.picker-open {
overflow: hidden;
}

.picker {
@extend %ui-depth4;

display: none;
position: fixed;
top: 1em;
left: 1em;
right: 1em;
bottom: 1em;
background-color: $gray-l4;
}

15 changes: 15 additions & 0 deletions cms/static/sass/views/_container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -580,3 +580,18 @@
width:100%;
height:100%
}

// Modal for adding components from libary using component picker
// cms/static/js/views/components/add_library_content_with_picker.js
.modal-add-component-picker {
top: 10%;
.modal-content {
padding: 0 !important;

& > iframe {
width: 100%;
min-height: 80vh;
background: url('#{$static-path}/images/spinner.gif') center center no-repeat;
}
}
}
1 change: 1 addition & 0 deletions cms/templates/container.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
isUnitPage: ${is_unit_page | n, dump_js_escaped_json},
canEdit: true,
outlineURL: "${outline_url | n, js_escaped_string}",
libraryContentPickerUrl: "${library_content_picker_url | n, js_escaped_string}",
clipboardData: ${user_clipboard | n, dump_js_escaped_json},
}
);
Expand Down
1 change: 1 addition & 0 deletions openedx/core/djangoapps/content_staging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def get_user_clipboard_json(user_id: int, request: HttpRequest | None = None):
except _UserClipboard.DoesNotExist:
# This user does not have any content on their clipboard.
return {"content": None, "source_usage_key": "", "source_context_title": "", "source_edit_url": ""}

serializer = _UserClipboardSerializer(
_user_clipboard_model_to_data(clipboard),
context={'request': request},
Expand Down