Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions cms/djangoapps/contentstore/toggles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
8 changes: 6 additions & 2 deletions cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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
Expand Down Expand Up @@ -298,14 +298,18 @@ 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,
'show_preview': context.get('show_preview', True),
'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),
Expand Down
55 changes: 40 additions & 15 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
},
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions cms/static/sass/elements/_xblocks.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
}
}

Expand Down
59 changes: 52 additions & 7 deletions cms/templates/studio_xblock_wrapper.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
</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:
Expand All @@ -92,23 +92,23 @@
<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>
<span class="sr">${_("Set Access")}</span>
</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>
<span class="sr">${_("Duplicate")}</span>
</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 ">
Expand All @@ -120,13 +120,58 @@
</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:
Expand Down