Files
edx-platform/cms/templates/studio_xblock_wrapper.html
Navin Karkera a11086ffac feat: allow editing imported text blocks (#37124)
* feat: allow editing html block imported from upstream

The modified field is left untouched in future sync while storing the
upstream values in hidden fields to allow authors to revert to upstream
version at any point.

* fix: sync downstream_customized field for copy-pasted modified block

* test: add more tests

* fix: lint issues

* test: copy paste

* feat: skip sync if html data is modified

* feat: update upstream fields only when modified

* refactor: use version_synced field to skip sync

* feat: edit title inplace for library source components

* fixup! feat: edit title inplace for library source components

* fix: edit title button style

* fix: test case

* fix: lint issue

* refactor: don't show different icon for modified upstream blocks

* Revert "refactor: use version_synced field to skip sync"

This reverts commit 8b784fff2f49b43702c952e7f955bd4048e8cc69.

* feat: only skip sync for modified blocks if updated as part of container

* refactor: update sync behaviour when synced individually and as part of parent

* feat: include ready to sync children info in downstream link get api

* test: fix failing tests

* fix: lint issues

* feat: new tests and update api to allow overriding modified fields in sync

* test: api changes

* refactor: edit options should be visible for individual imports

* docs: update api docs

* chore: remove old comments
2025-09-17 14:50:24 +05:30

284 lines
15 KiB
HTML

<%page expression_filter="h"/>
<%!
from django.utils.translation import gettext as _
from cms.djangoapps.contentstore.helpers import xblock_studio_url
from cms.djangoapps.contentstore.utils import is_visible_to_specific_partition_groups, get_editor_page_base_url, determine_label
from lms.lib.utils import is_unit
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor, use_video_gallery_flow
from cms.lib.xblock.upstream_sync import UpstreamLink
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
%>
<%
use_new_editor_text = use_new_text_editor(xblock.context_key)
use_new_editor_video = use_new_video_editor(xblock.context_key)
use_new_editor_problem = use_new_problem_editor(xblock.context_key)
use_new_video_gallery_flow = use_video_gallery_flow()
use_tagging = not is_tagging_feature_disabled()
xblock_url = xblock_studio_url(xblock)
show_inline = xblock.has_children and not xblock_url
section_class = "level-nesting" if show_inline else "level-element"
label = determine_label(xblock.display_name_with_default, xblock.scope_ids.block_type)
messages = xblock.validate().to_json()
has_not_configured_message = messages.get('summary',{}).get('type', None) == 'not-configured'
block_is_unit = is_unit(xblock)
upstream_info = UpstreamLink.try_get_for_block(xblock, log_error=False)
can_unlink = upstream_info.upstream_ref and not upstream_info.has_top_level_parent
%>
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
<script type="text/template" id="xblock-validation-messages-tpl">
<%static:include path="js/xblock-validation-messages.underscore" />
</script>
<script type="text/template" id="tag-count-tpl">
<%static:include path="js/tag-count.underscore" />
</script>
</%block>
<script type="text/javascript">
XBlockValidationFactory(
${messages | n, dump_js_escaped_json},
${bool(xblock_url) | n, dump_js_escaped_json}, // xblock_url will be None or a string
${bool(is_root) | n, dump_js_escaped_json}, // is_root will be None or a boolean
${bool(block_is_unit) | n, dump_js_escaped_json}, // block_is_unit will be None or a boolean
$('div.xblock-validation-messages[data-locator="${xblock.location | n, js_escaped_string}"]')
);
</script>
<%static:webpack entry="js/factories/tag_count">
TagCountFactory({
tags_count: "${tags_count | n, js_escaped_string}",
content_id: "${xblock.location | n, js_escaped_string}",
course_authoring_url: "${course_authoring_url | n, js_escaped_string}",
},
$('li.tag-count[data-locator="${xblock.location | n, js_escaped_string}"]')
);
</%static:webpack>
% if not is_root:
% if is_reorderable:
<li class="studio-xblock-wrapper is-draggable" id="${xblock.location}" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
% else:
<div class="studio-xblock-wrapper" id="${xblock.location}" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
% endif
<section class="wrapper-xblock is-collapsible ${section_class}
% if is_visible_to_specific_partition_groups(xblock):
has-group-visibility-set
% endif
">
<div class="ui-loading ${'is-hidden' if not is_loading else ''}">
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Importing components")}</span></p>
</div>
% endif
<header class="xblock-header xblock-header-${xblock.category} ${'is-hidden' if is_loading else ''}">
<div class="xblock-header-primary
% if not show_preview:
is-collapsed
% endif
"
use-new-editor-text = ${use_new_editor_text}
use-new-editor-video = ${use_new_editor_video}
use-new-editor-problem = ${use_new_editor_problem}
use-video-gallery-flow = ${use_new_video_gallery_flow}
authoring_MFE_base_url = ${get_editor_page_base_url(xblock.location.course_key)}
data-block-type = ${xblock.scope_ids.block_type}
data-usage-id = ${xblock.scope_ids.usage_id}
% if upstream_info.upstream_ref:
data-upstream-ref = ${upstream_info.upstream_ref}
data-version-synced = ${upstream_info.version_synced}
%endif
>
<div class="header-details">
% if show_inline:
<a href="#" data-tooltip="${_('Expand or Collapse')}" class="action expand-collapse collapse">
<span class="icon fa fa-caret-down ui-toggle-expansion" aria-hidden="true"></span>
<span class="sr">${_('Expand or Collapse')}</span>
</a>
% endif
<div class="xblock-display-title">
% if selectable:
<input type="checkbox" name="library-component" id="${xblock.location}"
class="header-library-checkbox" ${'checked="checked"' if is_selected else ''}>
<label for="${xblock.location}" class="xblock-display-name header-library-checkbox-label">
<span class="sr-only">${_("Select this problem")}</span>
${label}
</label>
% else:
% if upstream_info.upstream_ref:
% if upstream_info.error_message:
<!-- "broken link" icon from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:link_off:FILL@0;wght@400;GRAD@0;opsz@24&icon.size=24&icon.color=%235f6368&icon.query=link -->
<svg role="img" data-tooltip="${_("Sourced from a library - but the upstream link is broken/invalid.")}" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" style="vertical-align: middle; padding-bottom: 4px;">
<path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/>
</svg>
<span class="sr-only">${_("Sourced from a library - but the upstream link is broken/invalid.")}</span>
% else:
<!-- "library" icon from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:newsstand:FILL@0;wght@400;GRAD@0;opsz@24&icon.size=24 -->
<svg role="img" data-tooltip="${_("Sourced from a library - but has been modified locally." if upstream_info.is_modified else "Sourced from a library.")}" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle; padding-bottom: 4px;">
<path d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z"/>
</svg>
% if upstream_info.is_modified:
<span class="sr-only">${_("Sourced from a library - but has been modified locally.")}</span>
% else:
<span class="sr-only">${_("Sourced from a library.")}</span>
% endif
% endif
% endif
<span class="xblock-display-name">${label}</span>
% endif
% if selected_groups_label:
<p class="xblock-group-visibility-label">${selected_groups_label}</p>
% endif
</div>
</div>
<div class="header-actions">
<ul class="actions-list nav-dd ui-right">
% if can_edit_title and not can_edit:
<li class="action-item action-edit">
<button data-tooltip=${_('Edit Title')} class="btn-default action-edit title-edit-button only-for-lib-components">
<span class="icon fa fa-pencil" aria-hidden="true"></span>
<span class="action-button-text">${_("Edit Title")}</span>
</button>
</li>
% endif
% if not is_root:
% if upstream_info.ready_to_sync:
<li class="action-item">
<button
class="btn-default library-sync-button action-button"
data-tooltip="${_("Update available - click to sync")}"
>
<span class="icon fa fa-refresh" aria-hidden="true"></span>
<span class="action-button-text">${_("Update available")}</span>
</button>
</li>
% endif
% if use_tagging:
<li class="action-item tag-count" data-locator="${xblock.location}"></li>
% endif
% if not show_inline and can_edit:
<li class="action-item action-edit">
<button class="btn-default edit-button action-button" data-usage-id=${xblock.scope_ids.usage_id}>
<span class="icon fa fa-pencil" aria-hidden="true"></span>
<span class="action-button-text">${_("Edit")}</span>
</button>
</li>
% endif
<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:
% if can_edit_visibility:
<li class="nav-item">
<a class="access-button" href="#" role="button">${_("Manage Access")}</a>
</li>
% endif
% if can_move:
<li class="nav-item">
<a class="move-button" href="#" role="button">${_("Move")}</a>
</li>
% endif
% if use_tagging:
<li class="nav-item">
<a class="manage-tags-button" href="#" role="button">${_("Manage Tags")}</a>
</li>
% endif
% if is_course:
<!--
Only show the "Copy to Clipboard" button for xblocks inside courses since
the copy/paste functionality is not yet implemented for LibraryContent.
-->
<li class="nav-item">
<a class="copy-button" href="#" role="button">${_("Copy to Clipboard")}</a>
</li>
% endif
% if can_add:
<li class="nav-item">
<a class="duplicate-button" href="#" role="button">${_("Duplicate")}</a>
</li>
% endif
% endif
% if can_delete:
<li class="nav-item">
<a class="delete-button" href="#" role="button">${_("Delete")}</a>
</li>
% endif
% if can_unlink:
<li class="nav-item">
<a class="unlink-button" href="#" role="button">${_("Unlink from Library")}</a>
</li>
% endif
</ul>
</div>
</div>
</li>
% if is_reorderable and can_move:
<li class="action-item action-drag">
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
</li>
% endif
% endif
</ul>
</div>
</div>
% if not is_root:
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location}"/>
% if xblock_url and not has_not_configured_message:
<div class="xblock-header-secondary">
<div class="meta-info">${_('This block contains multiple components.')}</div>
<ul class="actions-list">
<li class="action-item action-view">
<a href="${xblock_url}" class="action-button xblock-view-action-button">
## Translators: this is a verb describing the action of viewing more details
<span class="action-button-text">${('View')}</span>
<span class="icon fa fa-arrow-right" aria-hidden="true"></span>
</a>
</li>
</ul>
</div>
% endif
% endif
</header>
% if is_root:
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location}"/>
% endif
% if show_preview:
% if is_root or not xblock_url:
% if not is_root and language:
<article class="xblock-render" lang="${language}">
% else:
<article class="xblock-render">
% endif
${content | n, decode.utf8}
</article>
% else:
<div class="xblock-message-area">
${content | n, decode.utf8}
</div>
% endif
% endif
% if not is_root:
<!-- footer for xblock_aside -->
</section>
% if is_reorderable:
</li>
% else:
</div>
% endif
% endif