feat: New actions menu for components in Studio (behind waffle flag) (#31853)
This commit is contained in:
@@ -159,3 +159,19 @@ def individualize_anonymous_user_id(course_id):
|
||||
Returns a boolean if individualized anonymous_user_id is enabled on the course
|
||||
"""
|
||||
return INDIVIDUALIZE_ANONYMOUS_USER_ID.is_enabled(course_id)
|
||||
|
||||
|
||||
# .. toggle_name: contentstore.enable_copy_paste_feature
|
||||
# .. toggle_implementation: WaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Moves most component-level actions into a submenu and adds new "Copy Component" and "Paste
|
||||
# Component" actions which can be used to copy components (XBlocks) within or among courses.
|
||||
# .. toggle_use_cases: temporary
|
||||
# .. toggle_creation_date: 2023-02-28
|
||||
# .. toggle_target_removal_date: 2023-05-01
|
||||
# .. toggle_tickets: https://github.com/openedx/modular-learning/issues/11 https://github.com/openedx/modular-learning/issues/50
|
||||
ENABLE_COPY_PASTE_FEATURE = WaffleFlag(
|
||||
f'{CONTENTSTORE_NAMESPACE}.enable_copy_paste_feature',
|
||||
__name__,
|
||||
CONTENTSTORE_LOG_PREFIX,
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ from xmodule.util.sandboxing import SandboxService
|
||||
from xmodule.util.xmodule_django import add_webpack_to_fragment
|
||||
from xmodule.x_module import AUTHOR_VIEW, PREVIEW_VIEWS, STUDENT_VIEW, ModuleSystem
|
||||
from cms.djangoapps.xblock_config.models import StudioConfig
|
||||
from cms.djangoapps.contentstore.toggles import individualize_anonymous_user_id
|
||||
from cms.djangoapps.contentstore.toggles import individualize_anonymous_user_id, ENABLE_COPY_PASTE_FEATURE
|
||||
from cms.lib.xblock.field_data import CmsFieldData
|
||||
from common.djangoapps.static_replace.services import ReplaceURLService
|
||||
from common.djangoapps.static_replace.wrapper import replace_urls_wrapper
|
||||
@@ -298,6 +298,9 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
if selected_groups_label:
|
||||
selected_groups_label = _('Access restricted to: {list_of_groups}').format(list_of_groups=selected_groups_label) # lint-amnesty, pylint: disable=line-too-long
|
||||
course = modulestore().get_course(xblock.location.course_key)
|
||||
can_edit = context.get('can_edit', True)
|
||||
# Copy-paste is a new feature; while we are beta-testing it, only beta users with the Waffle flag enabled see it
|
||||
enable_copy_paste = can_edit and ENABLE_COPY_PASTE_FEATURE.is_enabled()
|
||||
template_context = {
|
||||
'xblock_context': context,
|
||||
'xblock': xblock,
|
||||
@@ -305,7 +308,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
|
||||
'content': frag.content,
|
||||
'is_root': is_root,
|
||||
'is_reorderable': is_reorderable,
|
||||
'can_edit': context.get('can_edit', True),
|
||||
'can_edit': can_edit,
|
||||
'enable_copy_paste': enable_copy_paste,
|
||||
'can_edit_visibility': context.get('can_edit_visibility', xblock.scope_ids.usage_id.context_key.is_course),
|
||||
'selected_groups_label': selected_groups_label,
|
||||
'can_add': context.get('can_add', True),
|
||||
|
||||
@@ -18,8 +18,10 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
'click .edit-button': 'editXBlock',
|
||||
'click .access-button': 'editVisibilitySettings',
|
||||
'click .duplicate-button': 'duplicateXBlock',
|
||||
'click .copy-button': 'copyXBlock',
|
||||
'click .move-button': 'showMoveXBlockModal',
|
||||
'click .delete-button': 'deleteXBlock',
|
||||
'click .show-actions-menu-button': 'showXBlockActionsMenu',
|
||||
'click .new-component-button': 'scrollToNewComponentButtons'
|
||||
},
|
||||
|
||||
@@ -213,6 +215,23 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* If the new "Actions" menu is enabled, most XBlock actions like
|
||||
* Duplicate, Move, Delete, Manage Access, etc. are moved into this
|
||||
* menu. For this event, we just toggle displaying the menu.
|
||||
* @param {*} event
|
||||
*/
|
||||
showXBlockActionsMenu: function(event) {
|
||||
const showActionsButton = event.currentTarget;
|
||||
const subMenu = showActionsButton.parentElement.querySelector(".wrapper-nav-sub");
|
||||
// Code in 'base.js' normally handles toggling these dropdowns but since this one is
|
||||
// not present yet during the domReady event, we have to handle displaying it ourselves.
|
||||
subMenu.classList.toggle("is-shown");
|
||||
// if propagation is not stopped, the event will bubble up to the
|
||||
// body element, which will close the dropdown.
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
editVisibilitySettings: function(event) {
|
||||
this.editXBlock(event, {
|
||||
view: 'visibility_view',
|
||||
@@ -274,6 +293,12 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
});
|
||||
},
|
||||
|
||||
copyXBlock: function(event) {
|
||||
event.preventDefault();
|
||||
// This is a new feature, hidden behind a feature flag.
|
||||
alert("Copying of XBlocks is coming soon.");
|
||||
},
|
||||
|
||||
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
|
||||
@@ -320,8 +345,8 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
},
|
||||
|
||||
/*
|
||||
After move operation is complete, updates the xblock information from server .
|
||||
*/
|
||||
* After move operation is complete, updates the xblock information from server .
|
||||
*/
|
||||
onXBlockMoved: function() {
|
||||
this.model.fetch();
|
||||
},
|
||||
@@ -350,12 +375,12 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
},
|
||||
|
||||
/**
|
||||
* Refreshes the specified xblock's display. If the xblock is an inline child of a
|
||||
* reorderable container then the element will be refreshed inline. If not, then the
|
||||
* parent container will be refreshed instead.
|
||||
* @param element An element representing the xblock to be refreshed.
|
||||
* @param block_added Flag to indicate that new block has been just added.
|
||||
*/
|
||||
* Refreshes the specified xblock's display. If the xblock is an inline child of a
|
||||
* reorderable container then the element will be refreshed inline. If not, then the
|
||||
* parent container will be refreshed instead.
|
||||
* @param element An element representing the xblock to be refreshed.
|
||||
* @param block_added Flag to indicate that new block has been just added.
|
||||
*/
|
||||
refreshXBlock: function(element, block_added, is_duplicate) {
|
||||
var xblockElement = this.findXBlockElement(element),
|
||||
parentElement = xblockElement.parent(),
|
||||
@@ -370,13 +395,13 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh an xblock element inline on the page, using the specified xblockInfo.
|
||||
* Note that the element is removed and replaced with the newly rendered xblock.
|
||||
* @param xblockElement The xblock element to be refreshed.
|
||||
* @param block_added Specifies if a block has been added, rather than just needs
|
||||
* refreshing.
|
||||
* @returns {jQuery promise} A promise representing the complete operation.
|
||||
*/
|
||||
* Refresh an xblock element inline on the page, using the specified xblockInfo.
|
||||
* Note that the element is removed and replaced with the newly rendered xblock.
|
||||
* @param xblockElement The xblock element to be refreshed.
|
||||
* @param block_added Specifies if a block has been added, rather than just needs
|
||||
* refreshing.
|
||||
* @returns {jQuery promise} A promise representing the complete operation.
|
||||
*/
|
||||
refreshChildXBlock: function(xblockElement, block_added, is_duplicate) {
|
||||
var self = this,
|
||||
xblockInfo,
|
||||
|
||||
@@ -68,6 +68,24 @@
|
||||
width: 49%;
|
||||
|
||||
@include text-align(right);
|
||||
|
||||
// On components, if the copy/paste feature flag is enabled, we put the actions into a dropdown menu.
|
||||
.wrapper-nav-sub {
|
||||
@include text-align(left); // Undo the 'text-align: right' inherited from the parent
|
||||
|
||||
z-index: 10; // Stay in front of things like the video xblock or the "add component" buttons
|
||||
|
||||
.nav-item {
|
||||
a {
|
||||
// Match styling of ".wrapper-header nav .nav-item a" (dropdowns in Studio header)
|
||||
color: $gray-d1;
|
||||
|
||||
&:hover {
|
||||
color: $uxpl-blue-hover-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ block_is_unit = is_unit(xblock)
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<ul class="actions-list">
|
||||
<ul class="actions-list nav-dd ui-right">
|
||||
% if not is_root:
|
||||
% if can_edit:
|
||||
% if not show_inline:
|
||||
@@ -92,7 +92,7 @@ block_is_unit = is_unit(xblock)
|
||||
<span class="action-button-text">${_("Edit")}</span>
|
||||
</button>
|
||||
</li>
|
||||
% if can_edit_visibility:
|
||||
% if can_edit_visibility and not enable_copy_paste:
|
||||
<li class="action-item action-visibility">
|
||||
<button data-tooltip="${_("Access Settings")}" class="btn-default access-button action-button">
|
||||
<span class="icon fa fa-gear" aria-hidden="true"></span>
|
||||
@@ -100,7 +100,7 @@ block_is_unit = is_unit(xblock)
|
||||
</button>
|
||||
</li>
|
||||
% endif
|
||||
% if can_add:
|
||||
% if can_add and not enable_copy_paste:
|
||||
<li class="action-item action-duplicate">
|
||||
<button data-tooltip="${_("Duplicate")}" class="btn-default duplicate-button action-button">
|
||||
<span class="icon fa fa-copy" aria-hidden="true"></span>
|
||||
@@ -108,7 +108,7 @@ block_is_unit = is_unit(xblock)
|
||||
</button>
|
||||
</li>
|
||||
% endif
|
||||
% if can_move:
|
||||
% if can_move and not enable_copy_paste:
|
||||
<li class="action-item action-move">
|
||||
<button data-tooltip="${_("Move")}" class="btn-default move-button action-button">
|
||||
<span class="stack-move-icon fa-stack fa-lg ">
|
||||
@@ -120,15 +120,60 @@ block_is_unit = is_unit(xblock)
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
% if can_add:
|
||||
% if can_add and not enable_copy_paste:
|
||||
<!-- If we can add, we can delete. -->
|
||||
<li class="action-item action-delete">
|
||||
<button data-tooltip="${_("Delete")}" class="btn-default delete-button action-button">
|
||||
<span class="icon fa fa-trash-o" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Delete")}</span>
|
||||
<span class="icon fa fa-trash-o" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Delete")}</span>
|
||||
</button>
|
||||
</li>
|
||||
% endif
|
||||
% if enable_copy_paste:
|
||||
<!--
|
||||
If the "copy/paste" feature flag is enabled, all the actions besides "Edit" appear in a
|
||||
menu. We use .nav-dd on the parent element and .nav-item on this button to get the same
|
||||
dropdown menu appearance and behavior as in Studio's various other nav bars.
|
||||
-->
|
||||
<li class="action-item action-actions-menu nav-item">
|
||||
<button data-tooltip="${_("Actions")}" class="btn-default show-actions-menu-button action-button">
|
||||
<span class="icon fa fa-ellipsis-v" aria-hidden="true"></span>
|
||||
<span class="sr">${_("Actions")}</span>
|
||||
</button>
|
||||
<div class="wrapper wrapper-nav-sub" style="right: -10px; top: 45px;">
|
||||
<div class="nav-sub">
|
||||
<ul>
|
||||
% if not show_inline:
|
||||
<li class="nav-item">
|
||||
<a class="copy-button" href="#" role="button">${_("Copy")}</a>
|
||||
</li>
|
||||
% if can_add:
|
||||
<li class="nav-item">
|
||||
<a class="duplicate-button" href="#" role="button">${_("Duplicate")}</a>
|
||||
</li>
|
||||
% endif
|
||||
% if can_move:
|
||||
<li class="nav-item">
|
||||
<a class="move-button" href="#" role="button">${_("Move")}</a>
|
||||
</li>
|
||||
% endif
|
||||
% if can_edit_visibility:
|
||||
<li class="nav-item">
|
||||
<a class="access-button" href="#" role="button">${_("Manage Access")}</a>
|
||||
</li>
|
||||
% endif
|
||||
% endif
|
||||
% if can_add:
|
||||
<!-- If we can add, we can delete. -->
|
||||
<li class="nav-item">
|
||||
<a class="delete-button" href="#" role="button">${_("Delete")}</a>
|
||||
</li>
|
||||
% endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
% endif
|
||||
% if is_reorderable:
|
||||
<li class="action-item action-drag">
|
||||
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
|
||||
|
||||
Reference in New Issue
Block a user