diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py
index e2d1572bc25b..de70e8ce5fe6 100644
--- a/cms/djangoapps/contentstore/views/block.py
+++ b/cms/djangoapps/contentstore/views/block.py
@@ -14,7 +14,7 @@
from web_fragments.fragment import Fragment
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
-from common.djangoapps.edxmako.shortcuts import render_to_string
+from common.djangoapps.edxmako.shortcuts import render_to_response, render_to_string
from common.djangoapps.student.auth import (
has_studio_read_access,
has_studio_write_access,
@@ -38,10 +38,11 @@
STUDIO_VIEW,
) # lint-amnesty, pylint: disable=wrong-import-order
-
from ..helpers import (
is_unit,
)
+from ..utils import get_container_handler_context
+from .component import _get_item_in_course
from .preview import get_preview_fragment
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import (
@@ -300,6 +301,23 @@ def xblock_view_handler(request, usage_key_string, view_name):
return HttpResponse(status=406)
+@require_http_methods("GET")
+@login_required
+def edit_view_xblock(request, usage_key_string):
+ """
+ The handler for rendered edit xblock view.
+ """
+ usage_key = usage_key_with_run(usage_key_string)
+ if not has_studio_read_access(request.user, usage_key.course_key):
+ raise PermissionDenied()
+ 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)
+ return render_to_response('container_editor.html', container_handler_context)
+
+
@require_http_methods("GET")
@login_required
@expect_json
diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html
new file mode 100644
index 000000000000..a5afa5125e62
--- /dev/null
+++ b/cms/templates/container_editor.html
@@ -0,0 +1,317 @@
+## coding=utf-8
+## mako
+
+## Pages currently use v1 styling by default. Once the Pattern Library
+## rollout has been completed, this default can be switched to v2.
+<%! main_css = "style-main-v1" %>
+
+## Standard imports
+<%namespace name='static' file='static_content.html'/>
+<%!
+from django.utils.translation import gettext as _
+from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
+from lms.djangoapps.branding import api as branding_api
+from openedx.core.djangoapps.util.user_messages import PageLevelMessages
+from openedx.core.djangolib.js_utils import (
+ dump_js_escaped_json, js_escaped_string
+)
+from openedx.core.djangolib.markup import HTML
+from openedx.core.release import RELEASE_LINE
+%>
+<%def name="online_help_token()">
+<%
+ return "container"
+%>
+%def>
+<%!
+from django.urls import reverse
+from django.utils.translation import gettext as _
+from cms.djangoapps.contentstore.helpers import xblock_studio_url, xblock_type_display_name
+from openedx.core.djangolib.js_utils import (
+ dump_js_escaped_json, js_escaped_string
+)
+from openedx.core.djangolib.markup import HTML, Text
+%>
+
+<%page expression_filter="h"/>
+
+
+
+
+
+
+
+
+ <%block name="title">
+ ${xblock.display_name_with_default} ${xblock_type_display_name(xblock)}
+ %block> |
+ % if context_course:
+ <% ctx_loc = context_course.location %>
+ ${context_course.display_name_with_default} |
+ % elif context_library:
+ ${context_library.display_name_with_default} |
+ % endif
+ ${settings.STUDIO_NAME}
+
+
+ <%
+ jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE)
+ %>
+
+ % if getattr(settings, 'CAPTURE_CONSOLE_LOG', False):
+
+ % endif
+
+
+ % if settings.DEBUG:
+ ## Provides a fallback for gettext functions in development environment
+
+ % endif
+
+
+ <%block name="header_meta">%block>
+ <% favicon_url = branding_api.get_favicon_url() %>
+
+ <%static:css group='style-vendor'/>
+ <%static:css group='style-vendor-tinymce-content'/>
+ <%static:css group='style-vendor-tinymce-skin'/>
+
+
+ % if uses_bootstrap:
+
+ % else:
+ <%static:css group='${self.attr.main_css}'/>
+ % endif
+
+ <%include file="widgets/segment-io.html" />
+ <%block name="header_extras">
+
+ % for template_name in templates:
+
+ % endfor
+
+
+ % if not settings.STUDIO_FRONTEND_CONTAINER_URL:
+
+
+ % endif
+
+ %block>
+
+
+
+
+
+ <%block name="view_notes">%block>
+ ${_("Skip to main content")}
+ <%static:js group='base_vendor'/>
+ <%static:webpack entry="commons"/>
+
+
+
+
+ <%
+ banner_messages = list(PageLevelMessages.user_messages(request))
+ %>
+
+
+ <%block name="content">
+
+
+
+
+
+ %block>
+
+
+
+
+ <%block name="modal_placeholder">%block>
+ <%block name="jsextra">%block>
+
+ % if context_course:
+ <%static:webpack entry="js/factories/context_course"/>
+
+ % endif
+
+ % if user.is_authenticated:
+ <%static:webpack entry='js/sock'/>
+ % endif
+
+ <%block name='page_bundle'>
+
+ <%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);
+ });
+
+ $('.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');
+ });
+ });
+ };
+
+ xblockView.render();
+ });
+ %static:webpack>
+ %block>
+
+
+
+
diff --git a/cms/urls.py b/cms/urls.py
index 9828e9d0fbf0..7241242d5b59 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -18,6 +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.organization import OrganizationListView
from openedx.core.apidocs import api_info
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
@@ -145,6 +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}/(?P[^/]+)$', contentstore_views.xblock_view_handler,
name='xblock_view_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}?$', contentstore_views.xblock_handler,