From f5c17bb88caadbb75569b6d11dedc701b069b713 Mon Sep 17 00:00:00 2001 From: Ihor Romaniuk Date: Mon, 31 Mar 2025 23:31:59 +0200 Subject: [PATCH] feat: [FC-0070] add events and style for rendering Split xblock in chromeless template (#35813) This feature introduces functionalities to improve XBlock interactions within iframes: * Add styles that adopt default styles for Split Test which renders chromless template via iframe in MFE Authoring. * When the isIframeEmbed option is enabled, the XBlock sends a postMessage to the parent window. When sending such a message, the standard link transition is cancelled and the transition is carried out in MFE Authoring. --- cms/djangoapps/contentstore/helpers.py | 16 ++ .../contentstore/views/component.py | 15 +- .../xblock_storage_handlers/view_handlers.py | 1 + cms/static/images/advanced-icon.svg | 6 + cms/static/images/drag-and-drop-v2-icon.svg | 6 + cms/static/images/itembank-icon.svg | 3 + cms/static/images/library-icon.svg | 3 + cms/static/images/library_v2-icon.svg | 3 + cms/static/images/openassessment-icon.svg | 6 + cms/static/images/problem-icon.svg | 6 + cms/static/images/text-icon.svg | 3 + cms/static/images/video-icon.svg | 3 + cms/static/js/views/components/add_xblock.js | 46 +++- cms/static/js/views/pages/container.js | 253 ++++++++++-------- .../sass/course-unit-mfe-iframe-bundle.scss | 247 +++++++++++++---- .../elements/_course-unit-mfe-iframe.scss | 31 +++ .../partials/cms/theme/_variables-v1.scss | 21 +- cms/templates/container_chromeless.html | 1 + lms/templates/split_test_author_view.html | 2 +- 19 files changed, 500 insertions(+), 172 deletions(-) create mode 100644 cms/static/images/advanced-icon.svg create mode 100644 cms/static/images/drag-and-drop-v2-icon.svg create mode 100644 cms/static/images/itembank-icon.svg create mode 100644 cms/static/images/library-icon.svg create mode 100644 cms/static/images/library_v2-icon.svg create mode 100644 cms/static/images/openassessment-icon.svg create mode 100644 cms/static/images/problem-icon.svg create mode 100644 cms/static/images/text-icon.svg create mode 100644 cms/static/images/video-icon.svg diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index d2e7023c21..333cdbeb76 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -82,6 +82,22 @@ def is_unit(xblock, parent_xblock=None): return False +def is_library_content(xblock): + """ + Returns true if the specified xblock is library content. + """ + return xblock.category == 'library_content' + + +def get_parent_if_split_test(xblock): + """ + Returns the parent of the specified xblock if it is a split test, otherwise returns None. + """ + parent_xblock = get_parent_xblock(xblock) + if parent_xblock and parent_xblock.category == 'split_test': + return parent_xblock + + def xblock_has_own_studio_page(xblock, parent_xblock=None): """ Returns true if the specified xblock has an associated Studio page. Most xblocks do diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 914c078846..50aaaaded7 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -25,7 +25,11 @@ from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag -from cms.djangoapps.contentstore.helpers import is_unit +from cms.djangoapps.contentstore.helpers import ( + get_parent_if_split_test, + is_unit, + is_library_content, +) from cms.djangoapps.contentstore.toggles import ( libraries_v1_enabled, libraries_v2_enabled, @@ -148,11 +152,12 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st except ItemNotFoundError: return HttpResponseBadRequest() - is_unit_page = is_unit(xblock) - unit = xblock if is_unit_page else None + if use_new_unit_page(course.id): + if is_unit(xblock) or is_library_content(xblock): + return redirect(get_unit_url(course.id, xblock.location)) - if is_unit_page and use_new_unit_page(course.id): - return redirect(get_unit_url(course.id, unit.location)) + if split_xblock := get_parent_if_split_test(xblock): + return redirect(get_unit_url(course.id, split_xblock.location)) container_handler_context = get_container_handler_context(request, usage_key, course, xblock) container_handler_context.update({ diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index 9a5efe70fc..19e20804c2 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -611,6 +611,7 @@ def _create_block(request): modulestore().update_item(created_block, request.user.id) response["upstreamRef"] = upstream_ref response["static_file_notices"] = asdict(static_file_notices) + response["parent_locator"] = parent_locator return JsonResponse(response) diff --git a/cms/static/images/advanced-icon.svg b/cms/static/images/advanced-icon.svg new file mode 100644 index 0000000000..86096aefd2 --- /dev/null +++ b/cms/static/images/advanced-icon.svg @@ -0,0 +1,6 @@ + diff --git a/cms/static/images/drag-and-drop-v2-icon.svg b/cms/static/images/drag-and-drop-v2-icon.svg new file mode 100644 index 0000000000..733fb744d7 --- /dev/null +++ b/cms/static/images/drag-and-drop-v2-icon.svg @@ -0,0 +1,6 @@ + diff --git a/cms/static/images/itembank-icon.svg b/cms/static/images/itembank-icon.svg new file mode 100644 index 0000000000..b2198e1aba --- /dev/null +++ b/cms/static/images/itembank-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/images/library-icon.svg b/cms/static/images/library-icon.svg new file mode 100644 index 0000000000..a4fe81244e --- /dev/null +++ b/cms/static/images/library-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/images/library_v2-icon.svg b/cms/static/images/library_v2-icon.svg new file mode 100644 index 0000000000..4d69dbf9a0 --- /dev/null +++ b/cms/static/images/library_v2-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/images/openassessment-icon.svg b/cms/static/images/openassessment-icon.svg new file mode 100644 index 0000000000..4841b6b43d --- /dev/null +++ b/cms/static/images/openassessment-icon.svg @@ -0,0 +1,6 @@ + diff --git a/cms/static/images/problem-icon.svg b/cms/static/images/problem-icon.svg new file mode 100644 index 0000000000..7d51f436f7 --- /dev/null +++ b/cms/static/images/problem-icon.svg @@ -0,0 +1,6 @@ + diff --git a/cms/static/images/text-icon.svg b/cms/static/images/text-icon.svg new file mode 100644 index 0000000000..8588a471c9 --- /dev/null +++ b/cms/static/images/text-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/images/video-icon.svg b/cms/static/images/video-icon.svg new file mode 100644 index 0000000000..08f7444b62 --- /dev/null +++ b/cms/static/images/video-icon.svg @@ -0,0 +1,3 @@ + diff --git a/cms/static/js/views/components/add_xblock.js b/cms/static/js/views/components/add_xblock.js index 29ce5eec76..dd2f11dbe8 100644 --- a/cms/static/js/views/components/add_xblock.js +++ b/cms/static/js/views/components/add_xblock.js @@ -42,10 +42,38 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Add }, showComponentTemplates: function(event) { - var type; + var type, parentLocator, model, parentBlockType; event.preventDefault(); event.stopPropagation(); + type = $(event.currentTarget).data('type'); + parentLocator = $(event.currentTarget).closest('.xblock[data-usage-id]').data('usage-id'); + parentBlockType = $(event.currentTarget).parents('.xblock-author_view').last().data('block-type'); + model = this.collection.models.find(function(item) { return item.type === type; }) || {}; + + try { + if (this.options.isIframeEmbed && parentBlockType !== 'split_test') { + window.parent.postMessage( + { + type: 'showComponentTemplates', + payload: { + type: type, + parentLocator: parentLocator, + model: { + type: model.type, + display_name: model.display_name, + templates: model.templates, + support_legend: model.support_legend, + }, + } + }, document.referrer + ); + return true; + } + } catch (e) { + console.error(e); + } + this.$('.new-component').slideUp(250); this.$('.new-component-' + type).slideDown(250); this.$('.new-component-' + type + ' div').focus(); @@ -65,11 +93,25 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Add var self = this, $element = $(event.currentTarget), saveData = $element.data(), - oldOffset = ViewUtils.getScrollOffset(this.$el); + oldOffset = ViewUtils.getScrollOffset(this.$el), + usageId = $element.closest('.xblock[data-usage-id]').data('usage-id'); event.preventDefault(); this.closeNewComponent(event); if (saveData.type === 'library_v2') { + try { + if (this.options.isIframeEmbed) { + return window.parent.postMessage( + { + type: 'showSingleComponentPicker', + payload: { usageId }, + }, document.referrer + ); + } + } catch (e) { + console.error(e); + } + var modal = new AddLibraryContent(); modal.showComponentPicker( this.options.libraryContentPickerUrl, diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 2bd3cc18a3..684bd0ab7d 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -40,6 +40,7 @@ function($, _, Backbone, gettext, BasePage, 'change .header-library-checkbox': 'toggleLibraryComponent', 'click .collapse-button': 'collapseXBlock', 'click .xblock-view-action-button': 'viewXBlockContent', + 'click .xblock-view-group-link': 'viewXBlockContent', }, options: { @@ -60,8 +61,9 @@ function($, _, Backbone, gettext, BasePage, initialize: function(options) { BasePage.prototype.initialize.call(this, options); this.viewClass = options.viewClass || this.defaultViewClass; - this.isLibraryPage = (this.model.attributes.category === 'library'); - this.isLibraryContentPage = (this.model.attributes.category === 'library_content'); + this.isLibraryPage = this.model.attributes.category === 'library'; + this.isLibraryContentPage = this.model.attributes.category === 'library_content'; + this.isSplitTestContentPage = this.model.attributes.category === 'split_test'; this.nameEditor = new XBlockStringFieldEditor({ el: this.$('.wrapper-xblock-field'), model: this.model @@ -131,13 +133,16 @@ function($, _, Backbone, gettext, BasePage, if (this.options.isIframeEmbed) { window.addEventListener('message', (event) => { - const { data } = event; + const { data: initialData } = event; - if (!data) return; + if (!initialData) return; let xblockElement; let xblockWrapper; + const data = { ...initialData }; + data.payload = { ...data?.payload, ...data?.payload?.data }; + if (data.payload && data.payload.locator) { xblockElement = $(`[data-locator="${data.payload.locator}"]`); xblockWrapper = $("li.studio-xblock-wrapper[data-locator='" + data.payload.locator + "']"); @@ -173,6 +178,25 @@ function($, _, Backbone, gettext, BasePage, this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved); }, + postMessageToParent: function(body, callbackFn = null) { + try { + window.parent.postMessage(body, document.referrer); + if (callbackFn) { + callbackFn(); + } + } catch (e) { + console.error('Failed to post message:', e); + } + }, + + postMessageForHideProcessingNotification: function () { + this.postMessageToParent({ + type: 'hideProcessingNotification', + message: 'Hide processing notification', + payload: {}, + }); + }, + getViewParameters: function() { return { el: this.$('.wrapper-xblock'), @@ -237,18 +261,14 @@ function($, _, Backbone, gettext, BasePage, const scrollOffset = scrollOffsetString ? parseInt(scrollOffsetString, 10) : 0; if (scrollOffset) { - try { - window.parent.postMessage( - { - type: 'scrollToXBlock', - message: 'Scroll to XBlock', - payload: { scrollOffset } - }, document.referrer - ); - localStorage.removeItem('modalEditLastYPosition'); - } catch (e) { - console.error(e); - } + self.postMessageToParent( + { + type: 'scrollToXBlock', + message: 'Scroll to XBlock', + payload: { scrollOffset } + }, + () => localStorage.removeItem('modalEditLastYPosition') + ); } } }, @@ -272,13 +292,14 @@ function($, _, Backbone, gettext, BasePage, renderAddXBlockComponents: function() { var self = this; - if (self.options.canEdit && !self.options.isIframeEmbed) { + if (self.options.canEdit && (!self.options.isIframeEmbed || self.isSplitTestContentPage)) { this.$('.add-xblock-component').each(function(index, element) { var component = new AddXBlockComponent({ el: element, createComponent: _.bind(self.createComponent, self), collection: self.options.templates, libraryContentPickerUrl: self.options.libraryContentPickerUrl, + isIframeEmbed: self.options.isIframeEmbed, }); component.render(); }); @@ -288,7 +309,7 @@ function($, _, Backbone, gettext, BasePage, }, initializePasteButton() { - if (this.options.canEdit && !this.options.isIframeEmbed) { + if (this.options.canEdit && (!this.options.isIframeEmbed || this.isSplitTestContentPage)) { // We should have the user's clipboard status. const data = this.options.clipboardData; this.refreshPasteButton(data); @@ -305,7 +326,8 @@ 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 && !this.options.isIframeEmbed) { + if (!this.isLibraryPage && !this.isLibraryContentPage && (!this.options.isIframeEmbed || this.isSplitTestContentPage)) { + this.postMessageForHideProcessingNotification(); // '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) { @@ -340,17 +362,11 @@ function($, _, Backbone, gettext, BasePage, /** The user has clicked on the "Paste Component button" */ pasteComponent(event) { event.preventDefault(); - try { - if (this.options.isIframeEmbed) { - window.parent.postMessage( - { - type: 'pasteComponent', - payload: {} - }, document.referrer - ); - } - } catch (e) { - console.error(e); + if (this.options.isIframeEmbed) { + this.postMessageToParent({ + type: 'pasteComponent', + payload: {}, + }); } // Get the ID of the container (usually a unit/vertical) that we're pasting into: const parentElement = this.findXBlockElement(event.target); @@ -375,6 +391,9 @@ function($, _, Backbone, gettext, BasePage, placeholderElement.remove(); }); }).done((data) => { + if (this.options.isIframeEmbed) { + this.postMessageForHideProcessingNotification(); + } const { conflicting_files: conflictingFiles, error_files: errorFiles, @@ -646,17 +665,11 @@ function($, _, Backbone, gettext, BasePage, subMenu.classList.toggle('is-shown'); if (!subMenu.classList.contains('is-shown') && this.options.isIframeEmbed) { - try { - window.parent.postMessage( - { - type: 'toggleCourseXBlockDropdown', - message: 'Adjust the height of the dropdown menu', - payload: { courseXBlockDropdownHeight: 0 } - }, document.referrer - ); - } catch (error) { - console.error(error); - } + this.postMessageToParent({ + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { courseXBlockDropdownHeight: 0 } + }); } // Calculate the viewport height and the dropdown menu height. @@ -668,33 +681,21 @@ function($, _, Backbone, gettext, BasePage, if (courseUnitXBlockIframeHeight < courseXBlockDropdownHeight) { // If the dropdown menu is taller than the iframe, adjust the height of the dropdown menu. - try { - window.parent.postMessage( - { - type: 'toggleCourseXBlockDropdown', - message: 'Adjust the height of the dropdown menu', - payload: { courseXBlockDropdownHeight }, - }, document.referrer - ); - } catch (error) { - console.error(error); - } + this.postMessageToParent({ + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { courseXBlockDropdownHeight }, + }); } else if ((courseXBlockDropdownHeight + clickYPosition) > courseUnitXBlockIframeHeight) { if (courseXBlockDropdownHeight > courseUnitXBlockIframeHeight / 2) { // If the dropdown menu is taller than half the iframe, send a message to adjust its height. - try { - window.parent.postMessage( - { - type: 'toggleCourseXBlockDropdown', - message: 'Adjust the height of the dropdown menu', - payload: { - courseXBlockDropdownHeight: courseXBlockDropdownHeight / 2, - }, - }, document.referrer - ); - } catch (error) { - console.error(error); - } + this.postMessageToParent({ + type: 'toggleCourseXBlockDropdown', + message: 'Adjust the height of the dropdown menu', + payload: { + courseXBlockDropdownHeight: courseXBlockDropdownHeight / 2, + }, + }); } else { // Move the dropdown menu upward to prevent it from overflowing out of the viewport. if (this.options.isIframeEmbed) { @@ -719,18 +720,12 @@ function($, _, Backbone, gettext, BasePage, }, openManageTags: function(event) { - const contentId = this.findXBlockElement(event.target).data('locator'); - try { - if (this.options.isIframeEmbed) { - window.parent.postMessage( - { - type: 'openManageTags', - payload: { contentId } - }, document.referrer - ); - } - } catch (e) { - console.error(e); + const contentId = this.findXBlockElement(event.target).data('locator'); + if (this.options.isIframeEmbed) { + this.postMessageToParent({ + type: 'openManageTags', + payload: { contentId }, + }); } const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url'); @@ -747,13 +742,17 @@ function($, _, Backbone, gettext, BasePage, const usageId = encodeURI(primaryHeader.attr('data-usage-id')); try { if (this.options.isIframeEmbed) { - return window.parent.postMessage( + window.parent.postMessage( { - type: 'copyXBlock', + type: this.isSplitTestContentPage ? 'copyXBlockLegacy' : 'copyXBlock', message: 'Copy the XBlock', payload: { usageId } }, document.referrer ); + + if (!this.isSplitTestContentPage) { + return; + } } } catch (e) { console.error(e); @@ -795,6 +794,7 @@ function($, _, Backbone, gettext, BasePage, setTimeout(checkStatus, 1_000); return deferred; } else { + this.postMessageForHideProcessingNotification(); throw new Error(`Unexpected clipboard status "${status}" in successful API response.`); } }); @@ -909,15 +909,12 @@ function($, _, Backbone, gettext, BasePage, this.deleteComponent(this.findXBlockElement(event.target)); }, - createPlaceholderElement: function() { - return $('
', {class: 'studio-xblock-wrapper'}); - }, - createComponent: function(template, target, iframeMessageData) { // 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), + self = this, parentLocator = parentElement.data('locator'), buttonPanel = target?.closest('.add-xblock-component'), listPanel = buttonPanel?.prev(), @@ -929,28 +926,55 @@ function($, _, Backbone, gettext, BasePage, placeholderElement, $container; - if (this.options.isIframeEmbed) { + if (this.options.isIframeEmbed && !this.isSplitTestContentPage) { $container = $('ol.reorderable-container.ui-sortable'); scrollOffset = 0; } else { $container = listPanel; - scrollOffset = ViewUtils.getScrollOffset(buttonPanel); + if (!target.length && iframeMessageData.payload.parent_locator) { + $container = $('.xblock[data-usage-id="' + iframeMessageData.payload.parent_locator + '"]') + .find('ol.reorderable-container.ui-sortable'); + } + if (!iframeMessageData) { + scrollOffset = ViewUtils.getScrollOffset(buttonPanel); + } } placeholderElement = $placeholderEl.appendTo($container); - if (this.options.isIframeEmbed) { - if (iframeMessageData.payload.data && iframeMessageData.type === 'addXBlock') { - return this.onNewXBlock(placeholderElement, scrollOffset, false, iframeMessageData.payload.data); + if (this.options.isIframeEmbed && iframeMessageData) { + if (iframeMessageData.payload.data && iframeMessageData.type === 'addXBlock') { + return this.onNewXBlock(placeholderElement, scrollOffset, false, iframeMessageData.payload.data); + } + } + + if (this.options.isIframeEmbed && this.isSplitTestContentPage) { + this.postMessageToParent({ + type: 'addNewComponent', + message: 'Add new XBlock', + payload: {}, + }); + if (iframeMessageData) { + return; } } return $.postJSON(this.getURLRoot() + '/', requestData, _.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false)) + .always(function () { + if (self.options.isIframeEmbed && self.isSplitTestContentPage) { + self.postMessageToParent({ + type: 'hideProcessingNotification', + message: 'Hide processing notification', + payload: {} + }); + return true; + } + }) .fail(function() { // Remove the placeholder if the update failed placeholderElement.remove(); - }); + }); }, duplicateComponent: function(xblockElement) { @@ -966,17 +990,11 @@ function($, _, Backbone, gettext, BasePage, placeholderElement = $placeholderEl.insertAfter(xblockElement); if (this.options.isIframeEmbed) { - try { - window.parent.postMessage( - { - type: 'scrollToXBlock', - message: 'Scroll to XBlock', - payload: { scrollOffset: xblockElement.height() } - }, document.referrer - ); - } catch (e) { - console.error(e); - } + this.postMessageToParent({ + type: 'scrollToXBlock', + message: 'Scroll to XBlock', + payload: { scrollOffset: xblockElement.height() } + }); const messageHandler = ({ data }) => { if (data && data.type === 'completeXBlockDuplicating') { @@ -1028,7 +1046,6 @@ function($, _, Backbone, gettext, BasePage, 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) { @@ -1065,19 +1082,16 @@ function($, _, Backbone, gettext, BasePage, }, viewXBlockContent: function(event) { - try { - if (this.options.isIframeEmbed) { - event.preventDefault(); - var usageId = event.currentTarget.href.split('/').pop() || ''; - window.parent.postMessage({ - type: 'handleViewXBlockContent', - message: 'View the content of the XBlock', - payload: { usageId }, - }, document.referrer); - return true; - } - } catch (e) { - console.error(e); + if (this.options.isIframeEmbed) { + event.preventDefault(); + const usageId = event.currentTarget.href.split('/').pop() || ''; + const isViewGroupLink = event.currentTarget.classList.contains('xblock-view-group-link'); + this.postMessageToParent({ + type: isViewGroupLink ? 'handleViewGroupConfigurations' : 'handleViewXBlockContent', + message: isViewGroupLink ? 'View the group configurations page' : 'View the content of the XBlock', + payload: { usageId }, + }); + return true; } }, @@ -1142,6 +1156,17 @@ function($, _, Backbone, gettext, BasePage, destinationUrl = this.$('.xblock-header-primary').attr("authoring_MFE_base_url") + '/' + blockType[1] + '/' + encodeURI(data.locator); } + if (this.options.isIframeEmbed && this.isSplitTestContentPage) { + return this.postMessageToParent({ + type: 'handleRedirectToXBlockEditPage', + message: 'Redirect to xBlock edit page', + payload: { + type: blockType[1], + locator: encodeURI(data.locator), + }, + }); + } + window.location.href = destinationUrl; return; } diff --git a/cms/static/sass/course-unit-mfe-iframe-bundle.scss b/cms/static/sass/course-unit-mfe-iframe-bundle.scss index 79c20ea26b..fd0949e2e4 100644 --- a/cms/static/sass/course-unit-mfe-iframe-bundle.scss +++ b/cms/static/sass/course-unit-mfe-iframe-bundle.scss @@ -8,6 +8,11 @@ html { } } +body, +#main { + background-color: transparent; +} + [class*="view-"] .wrapper { .inner-wrapper { max-width: 100%; @@ -39,67 +44,105 @@ html { .actions-list .action-item .action-button { border-radius: 4px; + display: inline-flex; + align-items: center; + gap: ($baseline * .3); + padding: ($baseline * .15) ($baseline / 2); &:hover { background-color: $primary; color: $white; } - } - } - &.level-page .xblock-message { - padding: ($baseline * .75) ($baseline * 1.2); - border-radius: 0 0 4px 4px; - - &.information { - color: $text-color; - background-color: $xblock-message-info-bg; - border-color: $xblock-message-info-border-color; - } - - &.validation.has-warnings { - color: $black; - background-color: $xblock-message-warning-bg; - border-color: $xblock-message-warning-border-color; - border-top-width: 1px; - - .icon { - color: $xblock-message-warning-border-color; + .action-button-text { + line-height: 20px; } } - - a { - color: $primary; - } } - .xblock-author_view-library_content > .wrapper-xblock-message .xblock-message { - font-size: 16px; - line-height: 22px; - border-radius: 4px; - padding: ($baseline * 1.2); - box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15); - margin-bottom: ($baseline * 1.4); + &.level-page { + .xblock-message { + padding: ($baseline * .75) ($baseline * 1.2); + border-radius: 0 0 4px 4px; + + .xblock-message-list { + color: $black; + } + + &.information, + &.validation.has-warnings, + &.validation.has-errors { + color: $black; + border-width: 0; + font-size: 16px; + line-height: 22px; + padding: ($baseline * 1.2); + box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15); + } + + &.information { + background-color: $xblock-message-info-bg; + + .icon { + color: $xblock-message-info-icon-color; + } + } + + &.validation.has-warnings { + background-color: $xblock-message-warning-bg; + + .icon { + color: $xblock-message-warning-icon-color; + } + } + + &.validation.has-errors { + background-color: $xblock-message-error-bg; + + .icon { + color: $xblock-message-error-icon-color; + } + } + + a { + color: $primary; + } + } + + &.studio-xblock-wrapper > .wrapper-xblock-message .xblock-message, + .xblock > .wrapper-xblock-message .xblock-message { + border-radius: 4px; + margin-bottom: ($baseline * 1.4); + } + } + + .xblock-author_view-split_test .wrapper-xblock { + background: $white; + box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15); } &.level-element { box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15); margin: 0 0 ($baseline * 1.4) 0; - } - &.level-element .xblock-header-primary { - background-color: $white; - } + .xblock-header-primary { + background-color: $white; + } - &.level-element .xblock-render { - background: $white; - margin: 0; - padding: $baseline; - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; + .xblock-render { + background: $white; + margin: 0; + padding: $baseline; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } } .wrapper-xblock .header-actions .actions-list { + .wrapper-nav-sub { + z-index: 11; + } + .action-actions-menu:last-of-type .nav-sub { right: 120px; } @@ -176,6 +219,13 @@ html { } } } + + .wrapper-groups.is-inactive { + box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15); + border-radius: 6px; + border: none; + margin: ($baseline * 1.5) ($baseline / 2) 0; + } } .edit-xblock-modal select { @@ -443,8 +493,8 @@ html { } &.xmodule_DoneXBlock { - margin-top: 60px; - padding: 0 20px; + margin-top: ($baseline * 3); + padding: 0 $baseline; } .xblock-actions { @@ -578,7 +628,7 @@ html { } body [class*="view-"] .openassessment_editor_buttons.xblock-actions { - padding: 15px 2% 3px 2%; + padding: ($baseline * .75) 2% ($baseline * .15) 2%; } [class*="view-"] { @@ -634,7 +684,7 @@ body [class*="view-"] .openassessment_editor_buttons.xblock-actions { .list-input.settings-list { .field.comp-setting-entry.is-set .setting-input { color: $text-color; - margin-bottom: 5px; + margin-bottom: ($baseline * .25); } select { @@ -733,7 +783,7 @@ select { #openassessment_editor_header .editor_tabs .oa_editor_tab { @extend %light-button; - padding: 0 10px; + padding: 0 ($baseline / 2); } #openassessment_editor_header, @@ -762,7 +812,7 @@ select { #oa_rubric_editor_wrapper .openassessment_criterion_option .openassessment_criterion_option_point_wrapper label input { min-width: 70px; - font-size: 18px; + font-size: $base-font-size; height: 44px; } @@ -835,7 +885,7 @@ select { width: 100%; &.background-url { - margin-bottom: 10px; + margin-bottom: ($baseline / 2); } &.autozone-layout { @@ -858,3 +908,104 @@ select { width: 100%; } } + +.xblock-render { + .add-xblock-component { + background: transparent; + padding: $baseline; + + .new-component { + h5 { + margin-bottom: ($baseline * 1.2); + font-size: 22px; + font-weight: 700; + color: $black; + } + + .new-component-type { + display: flex; + flex-wrap: wrap; + gap: ($baseline * .6); + align-items: center; + justify-content: center; + + .add-xblock-component-button { + box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15); + width: 176px; + height: 110px; + color: $primary; + border-color: $primary; + background: transparent; + margin: 0; + display: inline-flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: ($baseline * .4); + + &:hover { + color: darken($primary, 10%); + background-color: lighten($primary, 80%); + border-color: darken($primary, 15%); + } + + .large-template-icon { + width: 24px; + height: 24px; + background: $primary; + + @each $name, $file in $template-icon-map { + &.large-#{$name}-icon { + mask: url("#{$static-path}/images/#{$file}.svg") center no-repeat; + } + } + } + + .name { + color: inherit; + font-size: 15.75px; + font-weight: 400; + } + + .beta { + color: $white; + background-color: $primary; + padding: ($baseline * .1) ($baseline * .4) ($baseline * .2); + font-size: 13.5px; + font-weight: 700; + line-height: 1; + margin: -($baseline * .3) 0 0; + } + } + } + } + + .new-component-templates { + border: 1px solid $border-color; + border-radius: 5px; + box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15); + margin: $baseline; + overflow: hidden; + + .button-component:hover { + background: $primary; + } + + .cancel-button { + @extend %primary-button; + } + } + } +} + +.paste-component { + margin: ($baseline * 1.2) ($baseline / 2) 0; + + .paste-component-whats-in-clipboard .clipboard-details-popup { + right: ($baseline / 2 * -1); + } + + .paste-component-button.button { + @extend %button-primary-outline; + } +} diff --git a/cms/static/sass/elements/_course-unit-mfe-iframe.scss b/cms/static/sass/elements/_course-unit-mfe-iframe.scss index dc75109942..5985e9aadd 100644 --- a/cms/static/sass/elements/_course-unit-mfe-iframe.scss +++ b/cms/static/sass/elements/_course-unit-mfe-iframe.scss @@ -31,6 +31,8 @@ cursor: pointer; background-image: none; display: block; + box-shadow: none; + text-shadow: none; &:hover { background: darken($primary, 5%); @@ -46,6 +48,35 @@ } } +%button-primary-outline { + @extend %modal-actions-button; + + color: $primary; + border-color: $primary; + text-shadow: none; + font-weight: 400; + position: relative; + + &:focus { + color: $primary; + background: transparent; + + &:before { + content: ""; + position: absolute; + inset: -5px; + border: 2px solid $primary; + border-radius: 10px; + } + } + + &:hover { + color: darken($primary, 10%); + background-color: lighten($primary, 80%); + border-color: darken($primary, 15%); + } +} + %light-button { @extend %modal-actions-button; diff --git a/cms/static/sass/partials/cms/theme/_variables-v1.scss b/cms/static/sass/partials/cms/theme/_variables-v1.scss index 0b3fe6b6e4..b60bc15f2e 100644 --- a/cms/static/sass/partials/cms/theme/_variables-v1.scss +++ b/cms/static/sass/partials/cms/theme/_variables-v1.scss @@ -317,6 +317,23 @@ $dark: #212529; $zindex-dropdown: 100; $xblock-message-info-bg: #eff8fa; -$xblock-message-info-border-color: #9cd2e6; +$xblock-message-info-icon-color: #9cd2e6; + $xblock-message-warning-bg: #fffdf0; -$xblock-message-warning-border-color: #fff6bf; +$xblock-message-warning-icon-color: #ffd900; + +$xblock-message-error-bg: #fbf2f3; +$xblock-message-error-icon-color: #c32d3a; + +$template-icon-map: ( + "library": "library-icon", + "library_v2": "library_v2-icon", + "itembank": "itembank-icon", + "advanced": "advanced-icon", + "html": "text-icon", + "openassessment": "openassessment-icon", + "problem": "problem-icon", + "video": "video-icon", + "drag-and-drop-v2": "drag-and-drop-v2-icon", + "text": "text-icon" +); diff --git a/cms/templates/container_chromeless.html b/cms/templates/container_chromeless.html index 2fe821e49d..a233e0dc7d 100644 --- a/cms/templates/container_chromeless.html +++ b/cms/templates/container_chromeless.html @@ -201,6 +201,7 @@ from openedx.core.release import RELEASE_LINE outlineURL: "${outline_url | n, js_escaped_string}", clipboardData: ${user_clipboard | n, dump_js_escaped_json}, isIframeEmbed: true, + libraryContentPickerUrl: "${library_content_picker_url | n, js_escaped_string}", } ); diff --git a/lms/templates/split_test_author_view.html b/lms/templates/split_test_author_view.html index 0c90081cdd..ddadf055ff 100644 --- a/lms/templates/split_test_author_view.html +++ b/lms/templates/split_test_author_view.html @@ -17,7 +17,7 @@ show_link = group_configuration_url is not None

${Text(_("This content experiment uses group configuration '{group_configuration_name}'.")).format( - group_configuration_name=Text(HTML("{}")).format(group_configuration_url, user_partition.name) if show_link else user_partition.name + group_configuration_name=Text(HTML("{}")).format(group_configuration_url, user_partition.name) if show_link else user_partition.name )}