feat: New actions menu for components in Studio (behind waffle flag) (#31853)

This commit is contained in:
Braden MacDonald
2023-03-03 10:09:27 -08:00
committed by GitHub
parent 89e308e45e
commit 6805654d40
5 changed files with 132 additions and 24 deletions

View File

@@ -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,
)

View File

@@ -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),

View File

@@ -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,

View File

@@ -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;
}
}
}
}
}
}

View File

@@ -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>