Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add move option to a modal window #2547

Merged
merged 4 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 7 additions & 1 deletion cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.db import transaction
from django.http import Http404, HttpResponse
from django.utils.translation import gettext as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.http import require_http_methods
from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
Expand Down Expand Up @@ -301,20 +302,25 @@ def xblock_view_handler(request, usage_key_string, view_name):
return HttpResponse(status=406)


@xframe_options_exempt
@require_http_methods("GET")
@login_required
def edit_view_xblock(request, usage_key_string):
def xblock_actions_view(request, usage_key_string, action_name):
"""
The handler for rendered edit xblock view.
Copy link
Collaborator Author

@GlugovGrGlib GlugovGrGlib May 7, 2024

Choose a reason for hiding this comment

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

Please update Docstring to describe the rendering for different xblock actions

"""
Return rendered xblock action view.
The action name should be provided as an argument.
Valid options for action name are edit and move.
"""

"""
usage_key = usage_key_with_run(usage_key_string)
if not has_studio_read_access(request.user, usage_key.course_key):
raise PermissionDenied()
if action_name not in ['edit', 'move']:
return HttpResponse(status=404)

store = modulestore()

with store.bulk_operations(usage_key.course_key):
course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
container_handler_context.update({'action_name': action_name})
return render_to_response('container_editor.html', container_handler_context)


Expand Down
4 changes: 4 additions & 0 deletions cms/static/js/views/modals/base_modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview'],
event.preventDefault();
event.stopPropagation(); // Make sure parent modals don't see the click
}
window.parent.postMessage({
method: 'close_edit_modal',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This message is incorrect, it should be generic for modals

msg: 'Sends a message when the modal window is closed'
}, '*');
this.hide();
},

Expand Down
5 changes: 5 additions & 0 deletions cms/static/js/views/modals/edit_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ function($, _, Backbone, gettext, BaseModal, ViewUtils, XBlockViewUtils, XBlockE
// Notify child views to stop listening events
Backbone.trigger('xblock:editorModalHidden');

window.parent.postMessage({
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Send message for edit

method: 'close_edit_modal',
msg: 'Sends a message when the modal window is closed'
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Please update message to specifically state about edit modal

}, '*');

BaseModal.prototype.hide.call(this);

// Notify the runtime that the modal has been hidden
Expand Down
25 changes: 21 additions & 4 deletions cms/static/js/views/utils/move_xblock_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,25 @@ function($, _, Backbone, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils,
.done(function(response) {
// hide modal
Backbone.trigger('move:hideMoveModal');
// hide xblock element
data.sourceXBlockElement.hide();
if (data.sourceXBlockElement) {
// hide xblock element
data.sourceXBlockElement.hide();
}

window.parent.postMessage({
method: 'move_xblock',
msg: 'Sends a message when the xblock is moved',
params: {
sourceDisplayName: data.sourceDisplayName,
sourceLocator: data.sourceLocator,
targetParentLocator: data.targetParentLocator,
}
}, '*');
window.parent.postMessage({
method: 'close_edit_modal',
msg: 'Sends a message when the modal window is closed'
}, '*');

showMovedNotification(
StringUtils.interpolate(
gettext('Success! "{displayName}" has been moved.'),
Expand All @@ -36,7 +53,7 @@ function($, _, Backbone, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils,
}
),
{
sourceXBlockElement: data.sourceXBlockElement,
sourceXBlockElement: data.sourceXBlockElement ? data.sourceXBlockElement : null,
sourceDisplayName: data.sourceDisplayName,
sourceLocator: data.sourceLocator,
sourceParentLocator: data.sourceParentLocator,
Expand Down Expand Up @@ -78,7 +95,7 @@ function($, _, Backbone, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils,
click: function() {
undoMoveXBlock(
{
sourceXBlockElement: data.sourceXBlockElement,
sourceXBlockElement: data.sourceXBlockElement ? data.sourceXBlockElement : null,
sourceDisplayName: data.sourceDisplayName,
sourceLocator: data.sourceLocator,
sourceParentLocator: data.sourceParentLocator,
Expand Down
138 changes: 22 additions & 116 deletions cms/templates/container_editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -150,68 +150,6 @@
<main id="main" aria-label="Content" tabindex="-1">
<div id="content">
<%block name="content">
<script type="text/javascript">
window.STUDIO_FRONTEND_IN_CONTEXT_IMAGE_SELECTION = true;
</script>

<div style="display:none" class="wrapper-mast wrapper">
<header class="mast has-actions has-navigation has-subtitle">
<nav class="nav-actions" aria-label="${_('Page Actions')}">
<ul>
<li class="action-item action-edit nav-item">
<a href="#" class="button button-edit action-button edit-button">
<span class="icon fa fa-pencil" aria-hidden="true"></span>
<span class="action-button-text">${_("Edit")}</span>
</a>
</li>
</ul>
</nav>
</header>
</div>

<script type="text/javascript">
$(document).ready(() => {
// Serves to initialize the rendering of a xblock edit modal window.
setTimeout(() => $('.button-edit').trigger('click'), 300);

/**
* Callback function for the MutationObserver to handle mutations
* and send information when the modal window close logic is triggered.
*
* @callback mutationCallback
* @param {MutationRecord[]} mutations - The list of mutations detected by the observer.
*/
const xblockEditModalObserver = new MutationObserver((mutations) => {
const modalClassName = 'wrapper-modal-window-edit-xblock';

// When a modal window is opened while the template is rendering,
// an element with class modalClassName is rendered,
// the MutationObserver defines this process in removedNodes.
const modalElementMutationRecords = mutations
.filter(({ removedNodes }) => {
const filteredModalClassName = Array.from(removedNodes).filter((node) =>
node.className && node.className.includes(modalClassName));

return filteredModalClassName.length > 0;
});

// If the element was present and deleted, close the modal window.
if (modalElementMutationRecords.length > 0 && !$('.' + modalClassName).length) {
window.parent.postMessage({
method: 'close_edit_modal',
msg: 'Sends a message when the modal window is closed'
}, '*');
}
});

xblockEditModalObserver.observe(document, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
});
</script>
</%block>
</div>
</main>
Expand Down Expand Up @@ -250,64 +188,32 @@
});
</script>
<%static:webpack entry="js/factories/container">
ContainerFactory(
${component_templates | n, dump_js_escaped_json},
${xblock_info | n, dump_js_escaped_json},
'${action | n, js_escaped_string}',
{
isUnitPage: ${is_unit_page | n, dump_js_escaped_json},
canEdit: true,
outlineURL: '${outline_url | n, js_escaped_string}',
clipboardData: ${user_clipboard | n, dump_js_escaped_json},
}
);

require(['js/models/xblock_info', 'js/views/xblock', 'js/views/utils/xblock_utils', 'common/js/components/utils/view_utils', 'gettext'], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext) {
var model = new XBlockInfo({ id: '${subsection.location|n, decode.utf8}' });
var xblockView = new XBlockView({
model: model,
el: $('#sequence-nav'),
view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}',
clipboardData: ${user_clipboard | n, dump_js_escaped_json},
});

xblockView.xblockReady = function() {
var toggleCaretButton = function(clipboardData) {
if (clipboardData && clipboardData.content && clipboardData.source_usage_key.includes('vertical')) {
$('.dropdown-toggle-button').show();
} else {
$('.dropdown-toggle-button').hide();
$('.dropdown-options').hide();
}
};
this.clipboardBroadcastChannel = new BroadcastChannel('studio_clipboard_channel');
this.clipboardBroadcastChannel.onmessage = (event) => toggleCaretButton(event.data);
toggleCaretButton(this.options.clipboardData);

$('#new-unit-button').on('click', function(event) {
event.preventDefault();
XBlockUtils.addXBlock($(this)).done(function(locator) {
ViewUtils.redirect('/container/' + locator + '?action=new');
});
});

$('.custom-dropdown .dropdown-toggle-button').on('click', function(event) {
event.stopPropagation(); // Prevent the event from closing immediately when we open it
$(this).next('.dropdown-options').slideToggle('fast'); // This toggles the dropdown visibility
var isExpanded = $(this).attr('aria-expanded') === 'true';
$(this).attr('aria-expanded', !isExpanded);
require(['js/models/xblock_info', 'js/views/xblock', 'js/views/utils/xblock_utils',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Need to remove unused dependencies

'common/js/components/utils/view_utils', 'gettext', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal'],
function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext, EditXBlockModal, MoveXBlockModal) {
function showMoveModal(unitLocation, xblockInfo, outlineUrl) {
var parentModel = new XBlockInfo({ id: unitLocation, category: 'vertical' });
var moveModal = new MoveXBlockModal({
sourceXBlockInfo: new XBlockInfo(xblockInfo),
sourceParentXBlockInfo: parentModel,
XBlockURLRoot: '/xblock',
outlineURL: outlineUrl,
});
moveModal.show();
}

$('.seq_paste_unit').on('click', function(event) {
event.preventDefault();
$('.dropdown-options').hide();
XBlockUtils.pasteXBlock($(this)).done(function(data) {
ViewUtils.redirect('/container/' + data.locator + '?action=new');
});
});
};
function showEditModal(xblockInfo) {
var editModal = new EditXBlockModal();
editModal.edit([], new XBlockInfo(xblockInfo), {});
}

xblockView.render();
var actionName = '${action_name|n, decode.utf8}';
if (actionName === 'move') {
showMoveModal('${unit.location|n, decode.utf8}', ${xblock_info | n, dump_js_escaped_json}, '${outline_url | n, js_escaped_string}');
} else if (actionName === 'edit') {
showEditModal(${xblock_info | n, dump_js_escaped_json});
}
});
</%static:webpack>
</%block>
Expand Down
6 changes: 3 additions & 3 deletions cms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import openedx.core.djangoapps.lang_pref.views
from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore import views as contentstore_views
from cms.djangoapps.contentstore.views.block import edit_view_xblock
from cms.djangoapps.contentstore.views.block import xblock_actions_view
from cms.djangoapps.contentstore.views.organization import OrganizationListView
from openedx.core.apidocs import api_info
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
Expand Down Expand Up @@ -146,8 +146,8 @@
name='xblock_outline_handler'),
re_path(fr'^xblock/container/{settings.USAGE_KEY_PATTERN}$', contentstore_views.xblock_container_handler,
name='xblock_container_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/editor$', edit_view_xblock,
name='xblock_editor_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/actions/(?P<action_name>[^/]+)$', xblock_actions_view,
name='xblock_actions_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/(?P<view_name>[^/]+)$', contentstore_views.xblock_view_handler,
name='xblock_view_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}?$', contentstore_views.xblock_handler,
Expand Down
Loading